diff --git a/src/Analyzer/ReferenceTrimmerAnalyzer.cs b/src/Analyzer/ReferenceTrimmerAnalyzer.cs index bf99019..ee6bdd7 100644 --- a/src/Analyzer/ReferenceTrimmerAnalyzer.cs +++ b/src/Analyzer/ReferenceTrimmerAnalyzer.cs @@ -752,6 +752,25 @@ private static void RegisterCSharpSyntaxTracking( } }, CSharp.SyntaxKind.InvocationExpression); + // Type qualifier in member access (e.g., `Foo.StaticMethod()` or `Foo.NestedType`). + // For static method calls and static member references, the receiver type appears at + // the syntax level as a qualifier — it isn't represented in IOperation (Instance is + // null for static, and the operation tree only carries TargetMethod/Member which point + // to the *defining* assembly, not the qualifier's assembly). Without this, calls like + // `Derived.InheritedStaticMethod()` would only credit the base class's assembly and + // the derived class's assembly would be wrongly flagged as removable. + context.RegisterSyntaxNodeAction(ctx => + { + if (ctx.Node is CSharp.Syntax.MemberAccessExpressionSyntax memberAccess) + { + SymbolInfo info = ctx.SemanticModel.GetSymbolInfo(memberAccess.Expression, ctx.CancellationToken); + if ((info.Symbol ?? info.CandidateSymbols.FirstOrDefault()) is ITypeSymbol typeSymbol) + { + trackType(typeSymbol); + } + } + }, CSharp.SyntaxKind.SimpleMemberAccessExpression); + // XML doc — only relevant when documentation generation is enabled, // matching the behavior of GetUsedAssemblyReferences() in the legacy path. context.RegisterSyntaxNodeAction(ctx => diff --git a/src/Tests/AnalyzerTests.cs b/src/Tests/AnalyzerTests.cs index cc15b46..e701694 100644 --- a/src/Tests/AnalyzerTests.cs +++ b/src/Tests/AnalyzerTests.cs @@ -488,6 +488,45 @@ public async Task UnusedBareReferenceReportsRT0001() Assert.AreEqual("RT0001", diagnostics[0].Id); } + [TestMethod] + public async Task UsedViaInheritedStaticMethod() + { + // The static method is *defined* on the base class in another assembly, but called + // through the derived class. Without tracking the type qualifier in the member access + // syntax, only the base assembly would be credited and the derived assembly would be + // wrongly flagged as unused. + var baseAsm = EmitDependency( + "namespace Dep { public class Base { public static void Foo() {} } }", + assemblyName: "BaseAsm"); + var derivedAsm = EmitDependency( + "namespace Dep { public class Derived : Base { } }", + assemblyName: "DerivedAsm", + additionalReferences: [baseAsm.Reference]); + var diagnostics = await RunAnalyzerAsync( + "class C { void M() { Dep.Derived.Foo(); } }", + [(baseAsm.Reference, baseAsm.Path, "ProjectReference", "../Base/Base.csproj"), + (derivedAsm.Reference, derivedAsm.Path, "ProjectReference", "../Derived/Derived.csproj")]); + AssertNoDiagnostics(diagnostics); + } + + [TestMethod] + public async Task UsedViaInheritedStaticField() + { + // Static field defined on base, accessed via the derived type. + var baseAsm = EmitDependency( + "namespace Dep { public class Base { public static int Counter; } }", + assemblyName: "BaseAsm"); + var derivedAsm = EmitDependency( + "namespace Dep { public class Derived : Base { } }", + assemblyName: "DerivedAsm", + additionalReferences: [baseAsm.Reference]); + var diagnostics = await RunAnalyzerAsync( + "class C { int M() => Dep.Derived.Counter; }", + [(baseAsm.Reference, baseAsm.Path, "ProjectReference", "../Base/Base.csproj"), + (derivedAsm.Reference, derivedAsm.Path, "ProjectReference", "../Derived/Derived.csproj")]); + AssertNoDiagnostics(diagnostics); + } + // ────────────────────────────────────────────────────────────────────── // Test infrastructure // ────────────────────────────────────────────────────────────────────── @@ -507,12 +546,24 @@ private static void AssertNoDiagnostics(ImmutableArray diagnostics) /// Compile dependency source into a DLL on disk and return the metadata reference + path. /// private static (MetadataReference Reference, string Path) EmitDependency(string source, string assemblyName = "Dependency") + => EmitDependency(source, assemblyName, additionalReferences: null); + + private static (MetadataReference Reference, string Path) EmitDependency( + string source, + string assemblyName, + MetadataReference[]? additionalReferences) { var tree = CSharpSyntaxTree.ParseText(source); + var references = new List { CorlibRef }; + if (additionalReferences != null) + { + references.AddRange(additionalReferences); + } + var compilation = CSharpCompilation.Create( assemblyName, [tree], - [CorlibRef], + references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); string path = Path.Combine(Path.GetTempPath(), $"RT_Test_{assemblyName}_{Guid.NewGuid():N}.dll");