Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
MA0182 handles entrypoint class and extensions (C# 14)
  • Loading branch information
meziantou committed Jan 15, 2026
commit 5a2959fea1113ba5bc01bc67d5419f6d357c2ef1
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
Expand All @@ -13,7 +12,7 @@ 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'.",
messageFormat: "Internal type '{0}' is apparently never used. If so, remove it from the assembly.",
RuleCategories.Design,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
Expand Down Expand Up @@ -57,7 +56,7 @@ private static bool IsPotentialUnusedType(INamedTypeSymbol symbol, INamedTypeSym
if (!symbol.CanBeReferencedByName)
return false;

if (symbol.IsStatic || symbol.IsImplicitlyDeclared)
if (symbol.IsImplicitlyDeclared)
return false;

// Exclude unit test classes
Expand Down Expand Up @@ -191,6 +190,7 @@ public void AnalyzeArrayCreation(OperationAnalysisContext context)
public void AnalyzeInvocation(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;
AddUsedType(operation, operation.TargetMethod.ContainingType);

// Track type arguments used in method invocations (e.g., JsonSerializer.Deserialize<T>())
foreach (var typeArgument in operation.TargetMethod.TypeArguments)
Expand Down Expand Up @@ -272,6 +272,12 @@ public void AnalyzeIsPattern(OperationAnalysisContext context)

public void AnalyzeCompilationEnd(CompilationAnalysisContext context)
{
var entryPoint = compilation.GetEntryPoint(context.CancellationToken);
if (entryPoint is not null)
{
AddUsedType(entryPoint.ContainingType);
}

foreach (var type in _potentialUnusedTypes)
{
if (_usedTypes.Contains(type))
Expand Down Expand Up @@ -311,6 +317,8 @@ private void AddUsedType(ISymbol? containingSymbol, ITypeSymbol typeSymbol)
AddUsedType(containingSymbol as ITypeSymbol, typeSymbol);
}

private void AddUsedType(ITypeSymbol typeSymbol) => AddUsedType((ITypeSymbol?)null, typeSymbol);

private void AddUsedType(ITypeSymbol? referenceLocation, ITypeSymbol typeSymbol)
{
if (referenceLocation is not null && referenceLocation.IsEqualTo(typeSymbol))
Expand Down Expand Up @@ -347,6 +355,18 @@ private void AddUsedType(ITypeSymbol? referenceLocation, ITypeSymbol typeSymbol)
{
AddUsedType(referenceLocation, typeArgument);
}

#if CSHARP14_OR_GREATER
// If extension (C# 14), also mark containing type as used
if (namedTypeSymbol.IsExtension)
{
var containingType = namedTypeSymbol.ContainingType;
if (containingType is not null)
{
AddUsedType(referenceLocation, containingType);
}
}
#endif
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Meziantou.Analyzer.Rules;
using Meziantou.Analyzer.Test.Helpers;
using Microsoft.CodeAnalysis;
using TestHelper;

namespace Meziantou.Analyzer.Test.Rules;
Expand Down Expand Up @@ -41,10 +42,10 @@ await CreateProjectBuilder()
}

[Fact]
public async Task StaticClass_NoDiagnostic()
public async Task StaticClass_Diagnostic()
{
const string SourceCode = """
internal static class StaticClass
internal static class [|StaticClass|]
{
public static void Method() { }
}
Expand Down Expand Up @@ -695,11 +696,12 @@ private static void Main(string[] args)
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(OutputKind.ConsoleApplication)
.ValidateAsync();
}

[Fact]
public async Task InternalClassUsedInArrayCreation_NoDiagnostic()
public async Task EntryPointClass_NoDiagnostic()
{
const string SourceCode = """
using System;
Expand All @@ -716,6 +718,30 @@ private static void Main(string[] args)
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(OutputKind.ConsoleApplication)
.ValidateAsync();
}

[Fact]
public async Task EntryPointInClassLibrary_Reported()
{
const string SourceCode = """
using System;

internal sealed class Config
{
}

internal static class [|Program|]
{
private static void Main(string[] args)
{
var list = Array.Empty<Config>();
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
Expand Down Expand Up @@ -1138,7 +1164,7 @@ internal class DataStore
public string Value { get; set; }
}

internal static class DataStoreExtensions
internal static class [|DataStoreExtensions|]
{
extension (DataStore datastore)
{
Expand All @@ -1153,6 +1179,28 @@ await CreateProjectBuilder()
.ValidateAsync();
}

[Fact]
public async Task InternalClassUsedInExplicitExtensionType_Diagnostic()
{
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 InternalClassUsedInExplicitExtensionType_NoDiagnostic()
{
Expand All @@ -1169,6 +1217,15 @@ internal static class DataStoreExtensions
public string GetValue() => settings.Key;
}
}

public class Sample
{
public void Test()
{
var settings = new Settings { Key = "Test" };
var value = settings.GetValue();
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
Expand All @@ -1184,7 +1241,7 @@ internal class Entity
public int Id { get; set; }
}

internal static class EntityExtension
internal static class [|EntityExtension|]
{
extension<T>(T entity) where T : Entity
{
Expand Down Expand Up @@ -1213,7 +1270,7 @@ internal class BaseEntity
public string Name { get; set; }
}

internal static class RepositoryExtension
internal static class [|RepositoryExtension|]
{
extension<T>(T entity) where T : BaseEntity, IIdentifiable, new()
{
Expand Down Expand Up @@ -1877,4 +1934,78 @@ await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}


[Fact]
public async Task InternalClassWithFactoryMethod_NoDiagnostic()
{
const string SourceCode = """
internal sealed class BugDemo
{
private BugDemo()
{
}

public static BugDemo Create() => new();
}

public class Consumer
{
public void Method()
{
var x = BugDemo.Create();
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task InternalClassWithFactoryMethodNotUsed_Diagnostic()
{
const string SourceCode = """
internal sealed class [|UnusedFactory|]
{
private UnusedFactory()
{
}

public static UnusedFactory Create() => new();
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task InternalClassWithFactoryMethodInternalUsage_NoDiagnostic()
{
const string SourceCode = """
internal sealed class ConfigurableCertificateValidatingHttpClientHandler
{
private ConfigurableCertificateValidatingHttpClientHandler()
{
}

public static ConfigurableCertificateValidatingHttpClientHandler CreateClient()
{
return new ConfigurableCertificateValidatingHttpClientHandler();
}
}

public class ApiClient
{
public void Setup()
{
var handler = ConfigurableCertificateValidatingHttpClientHandler.CreateClient();
}
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}
}