diff --git a/src/Analyzer/ReferenceTrimmerAnalyzer.cs b/src/Analyzer/ReferenceTrimmerAnalyzer.cs index ee6bdd7..757d271 100644 --- a/src/Analyzer/ReferenceTrimmerAnalyzer.cs +++ b/src/Analyzer/ReferenceTrimmerAnalyzer.cs @@ -333,6 +333,25 @@ void TrackPatternTypes(IPatternOperation pattern) TrackAttribute(attr); } + // For delegates, the parameter and return types live on the implicitly-declared + // Invoke method, which the SymbolKind.Method action does not fire for. Walk them here. + // Mirrors the IMethodSymbol case below (return type, parameter types, return-type + // attributes); type-parameter constraints are already covered above for the delegate + // type itself, and parameter attributes are not tracked for ordinary methods either. + if (namedType.TypeKind == TypeKind.Delegate && namedType.DelegateInvokeMethod is IMethodSymbol invoke) + { + TrackType(invoke.ReturnType); + foreach (IParameterSymbol param in invoke.Parameters) + { + TrackType(param.Type); + } + + foreach (AttributeData attr in invoke.GetReturnTypeAttributes()) + { + TrackAttribute(attr); + } + } + break; case IMethodSymbol method: diff --git a/src/Tests/AnalyzerTests.cs b/src/Tests/AnalyzerTests.cs index e701694..9779cab 100644 --- a/src/Tests/AnalyzerTests.cs +++ b/src/Tests/AnalyzerTests.cs @@ -488,6 +488,66 @@ public async Task UnusedBareReferenceReportsRT0001() Assert.AreEqual("RT0001", diagnostics[0].Id); } + [TestMethod] + public async Task UsedViaDelegateParameterType() + { + // The external type is referenced only via a delegate's parameter type. + // The delegate's Invoke method is implicitly declared, so a SymbolKind.Method + // action does not fire for it — the analyzer must track parameters/return type + // through the INamedTypeSymbol with TypeKind.Delegate. + var dep = EmitDependency("namespace Dep { public class Service {} }"); + var diagnostics = await RunAnalyzerAsync( + "public delegate void Configure(Dep.Service s);", + dep); + AssertNoDiagnostics(diagnostics); + } + + [TestMethod] + public async Task UsedViaDelegateReturnType() + { + // The external type is referenced only via a delegate's return type. + var dep = EmitDependency("namespace Dep { public class Result {} }"); + var diagnostics = await RunAnalyzerAsync( + "public delegate Dep.Result Produce();", + dep); + AssertNoDiagnostics(diagnostics); + } + + [TestMethod] + public async Task UsedViaDelegateGenericReturnType() + { + // The external type is referenced only via a generic argument in the delegate's return type. + var dep = EmitDependency("namespace Dep { public class Item {} }"); + var diagnostics = await RunAnalyzerAsync( + "public delegate System.Collections.Generic.List ProduceItems();", + dep); + AssertNoDiagnostics(diagnostics); + } + + [TestMethod] + public async Task UsedViaDelegateTypeParameterConstraint() + { + // External type referenced only via a type parameter constraint on a generic delegate. + var dep = EmitDependency("namespace Dep { public class Base {} }"); + var diagnostics = await RunAnalyzerAsync( + "public delegate void Apply(T x) where T : Dep.Base;", + dep); + AssertNoDiagnostics(diagnostics); + } + + [TestMethod] + public async Task UnusedDelegateNoExternalTypesReportsDiagnostic() + { + // Negative test: a delegate using only same-assembly types should NOT mark + // an unrelated external assembly as used. + var dep = EmitDependency("namespace Dep { public class Service {} }"); + var diagnostics = await RunAnalyzerAsync( + "public class Local {} public delegate Local Produce(Local x);", + dep); + Assert.AreEqual(1, diagnostics.Length); + Assert.AreEqual("RT0002", diagnostics[0].Id); + } + [TestMethod] public async Task UsedViaInheritedStaticMethod() {