diff --git a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs index cc1e05a6..752491c3 100644 --- a/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/AvoidUnusedInternalTypesAnalyzer.cs @@ -29,7 +29,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(ctx => { - var analyzerContext = new AnalyzerContext(); + var analyzerContext = new AnalyzerContext(ctx.Compilation); ctx.RegisterSymbolAction(analyzerContext.AnalyzeNamedTypeSymbol, SymbolKind.NamedType); ctx.RegisterSymbolAction(analyzerContext.AnalyzePropertyOrFieldSymbol, SymbolKind.Property, SymbolKind.Field); @@ -67,11 +67,13 @@ private static bool IsPotentialUnusedType(INamedTypeSymbol symbol, CancellationT return true; } - private sealed class AnalyzerContext + private sealed class AnalyzerContext(Compilation compilation) { private readonly List _potentialUnusedTypes = []; private readonly HashSet _usedTypes = new(SymbolEqualityComparer.Default); + private INamedTypeSymbol? CoClassAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Runtime.InteropServices.CoClassAttribute"); + public void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context) { var symbol = (INamedTypeSymbol)context.Symbol; @@ -98,6 +100,24 @@ public void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context) } } + // Track CoClass attribute on interfaces - the interface and CoClass implementation form a COM interop pair + if (symbol.TypeKind == TypeKind.Interface && CoClassAttributeSymbol is not null) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass.IsEqualTo(CoClassAttributeSymbol)) + { + var attributeValue = attribute.ConstructorArguments.FirstOrDefault(); + if (!attributeValue.IsNull && attributeValue.Kind == TypedConstantKind.Type && attributeValue.Value is ITypeSymbol coClassType) + { + // Mark both the interface and the CoClass implementation as used + AddUsedType(symbol); + AddUsedType(coClassType); + } + } + } + } + // Track types used in generic constraints foreach (var typeParameter in symbol.TypeParameters) { diff --git a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs index 6c5baf8d..8efdb131 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AvoidUnusedInternalTypesAnalyzerTests.cs @@ -1371,6 +1371,32 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task InterfaceWithCoClassAttribute_NoDiagnostic() + { + const string SourceCode = """ + using System.Runtime.InteropServices; + + [ComImport] + [Guid("00000000-0000-0000-0000-000000000001")] + [CoClass(typeof(FileSaveDialogRCW))] + internal interface NativeFileSaveDialog + { + } + + [ComImport] + [ClassInterface(ClassInterfaceType.None)] + [Guid("00000000-0000-0000-0000-000000000002")] + internal sealed class FileSaveDialogRCW + { + } + """; + + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + [Fact] public async Task InternalStructUsedAsPointerInMethodParameter_NoDiagnostic() {