Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions src/Analyzer/ReferenceTrimmerAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
60 changes: 60 additions & 0 deletions src/Tests/AnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dep.Item> 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>(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()
{
Expand Down
Loading