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 @@ -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 <cref> — only relevant when documentation generation is enabled,
// matching the behavior of GetUsedAssemblyReferences() in the legacy path.
context.RegisterSyntaxNodeAction(ctx =>
Expand Down
53 changes: 52 additions & 1 deletion src/Tests/AnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ──────────────────────────────────────────────────────────────────────
Expand All @@ -507,12 +546,24 @@ private static void AssertNoDiagnostics(ImmutableArray<Diagnostic> diagnostics)
/// Compile dependency source into a DLL on disk and return the metadata reference + path.
/// </summary>
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<MetadataReference> { 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");
Expand Down
Loading