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
144 changes: 142 additions & 2 deletions src/Analyzer/ReferenceTrimmerAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;
using ReferenceTrimmer.Shared;
using CSharp = Microsoft.CodeAnalysis.CSharp;

namespace ReferenceTrimmer.Analyzer;

Expand Down Expand Up @@ -70,7 +72,7 @@ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
context.RegisterCompilationStartAction(CompilationStart);
}

Expand Down Expand Up @@ -214,6 +216,14 @@ void TrackType(ITypeSymbol? type)
case IPointerTypeSymbol pointer:
type = pointer.PointedAtType;
continue;
case IFunctionPointerTypeSymbol funcPtr:
TrackType(funcPtr.Signature.ReturnType);
foreach (IParameterSymbol fpParam in funcPtr.Signature.Parameters)
{
TrackType(fpParam.Type);
}

return;
default:
TrackAssembly(type.ContainingAssembly);
if (type is INamedTypeSymbol named)
Expand Down Expand Up @@ -416,6 +426,35 @@ void TrackPatternTypes(IPatternOperation pattern)
case ICatchClauseOperation catchClause:
TrackType(catchClause.ExceptionType);
break;

case ISwitchExpressionArmOperation switchArm:
TrackPatternTypes(switchArm.Pattern);
break;

case IPatternCaseClauseOperation patternClause:
TrackPatternTypes(patternClause.Pattern);
break;

case ILocalFunctionOperation localFunc:
TrackType(localFunc.Symbol.ReturnType);
foreach (IParameterSymbol lfParam in localFunc.Symbol.Parameters)
{
TrackType(lfParam.Type);
}

break;

case IAnonymousFunctionOperation lambda:
foreach (IParameterSymbol lambdaParam in lambda.Symbol.Parameters)
{
TrackType(lambdaParam.Type);
}

break;

case ISizeOfOperation sizeOfOp:
TrackType(sizeOfOp.TypeOperand);
break;
}
},
OperationKind.Invocation,
Expand All @@ -428,7 +467,20 @@ void TrackPatternTypes(IPatternOperation pattern)
OperationKind.Conversion,
OperationKind.IsType,
OperationKind.IsPattern,
OperationKind.CatchClause);
OperationKind.CatchClause,
OperationKind.SwitchExpressionArm,
OperationKind.CaseClause,
OperationKind.LocalFunction,
OperationKind.AnonymousFunction,
OperationKind.SizeOf);

// Track nameof() and XML doc cref references via language-specific syntax actions.
// These require syntax-level analysis because nameof is lowered to a string literal
// and crefs live in documentation trivia — neither surfaces through IOperation.
if (compilation.Language == LanguageNames.CSharp)
{
RegisterCSharpSyntaxTracking(context, TrackAssembly, TrackType);
}

context.RegisterCompilationEndAction(endContext =>
{
Expand All @@ -446,6 +498,28 @@ void TrackPatternTypes(IPatternOperation pattern)
return;
}

// Mark type-forwarding assemblies as used when the destination assembly is used.
// E.g. a package may forward types to the runtime; the code uses the type (tracking the
// runtime assembly) but the forwarder assembly must also be kept as a reference.
foreach (KeyValuePair<string, IAssemblySymbol> kvp in pathToAssembly)
{
if (usedReferencePaths.ContainsKey(kvp.Key))
{
continue;
}

foreach (INamedTypeSymbol forwardedType in kvp.Value.GetForwardedTypes())
{
if (forwardedType.ContainingAssembly != null
&& assemblyToPath.TryGetValue(forwardedType.ContainingAssembly.Identity, out string? destPath)
&& usedReferencePaths.ContainsKey(destPath))
{
usedReferencePaths.TryAdd(kvp.Key, 0);
break;
}
}
}

HashSet<string> usedReferences = new(usedReferencePaths.Keys, StringComparer.OrdinalIgnoreCase);

// For bare Reference items (RT0001), we always need a conservative "transitively used" set
Expand Down Expand Up @@ -555,6 +629,72 @@ private static void ReportUnusedReferences(
}
}

// ──────────────────────────────────────────────────────────────────────
// Language-specific syntax tracking (nameof, crefs)
// ──────────────────────────────────────────────────────────────────────

// Separate methods per language to avoid JIT-loading the wrong language assembly.

[MethodImpl(MethodImplOptions.NoInlining)]
private static void RegisterCSharpSyntaxTracking(
CompilationStartAnalysisContext context,
Action<IAssemblySymbol?> trackAssembly,
Action<ITypeSymbol?> trackType)
{
// nameof() — appears as InvocationExpression at the syntax level but is
// lowered to a string literal in the IOperation tree.
context.RegisterSyntaxNodeAction(ctx =>
{
if (ctx.Node is CSharp.Syntax.InvocationExpressionSyntax invocation
&& invocation.Expression is CSharp.Syntax.IdentifierNameSyntax id
&& id.Identifier.Text == "nameof"
&& invocation.ArgumentList.Arguments.Count > 0)
{
// Verify it is actually the nameof operator, not a method called "nameof".
SymbolInfo invocationInfo = ctx.SemanticModel.GetSymbolInfo(invocation, ctx.CancellationToken);
if (invocationInfo.Symbol is IMethodSymbol)
{
return;
}

SymbolInfo argInfo = ctx.SemanticModel.GetSymbolInfo(invocation.ArgumentList.Arguments[0].Expression, ctx.CancellationToken);
ISymbol? symbol = argInfo.Symbol ?? argInfo.CandidateSymbols.FirstOrDefault();
if (symbol is ITypeSymbol typeSymbol)
{
trackType(typeSymbol);
}
else if (symbol != null)
{
trackAssembly(symbol.ContainingAssembly);
}
}
}, CSharp.SyntaxKind.InvocationExpression);

// XML doc <cref> — only relevant when documentation generation is enabled,
// matching the behavior of GetUsedAssemblyReferences() in the legacy path.
context.RegisterSyntaxNodeAction(ctx =>
{
if (ctx.SemanticModel.SyntaxTree.Options.DocumentationMode == DocumentationMode.None)
{
return;
}

if (ctx.Node is CSharp.Syntax.XmlCrefAttributeSyntax cref)
{
SymbolInfo symbolInfo = ctx.SemanticModel.GetSymbolInfo(cref.Cref, ctx.CancellationToken);
ISymbol? symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault();
if (symbol is ITypeSymbol typeSymbol)
{
trackType(typeSymbol);
}
else if (symbol != null)
{
trackAssembly(symbol.ContainingAssembly);
}
}
}, CSharp.SyntaxKind.XmlCrefAttribute);
}

// ──────────────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────────────
Expand Down
36 changes: 36 additions & 0 deletions src/Tests/E2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,42 @@ public Task UsedProjectReferenceNoReferenceAssembly(bool useSymbolAnalysis)
useSymbolAnalysis: useSymbolAnalysis);
}

[TestMethod]
[DataRow(false)]
[DataRow(true)]
public Task UsedProjectReferenceSwitchPattern(bool useSymbolAnalysis)
{
// Dependency type used only in switch expression type pattern and switch case clause pattern.
return RunMSBuildAsync(
projectFile: "Library/Library.csproj",
expectedWarnings: [],
useSymbolAnalysis: useSymbolAnalysis);
}

[TestMethod]
public Task UsedProjectReferenceNameof()
{
// Dependency type used only in nameof(). nameof is lowered to a string literal
// in IOperation, so only the syntax-level handler catches it. Symbol-analysis only.
return RunMSBuildAsync(
projectFile: "Library/Library.csproj",
expectedWarnings: [],
useSymbolAnalysis: true);
}

[TestMethod]
[DataRow(false)]
[DataRow(true)]
public Task UsedProjectReferenceCref(bool useSymbolAnalysis)
{
// Dependency type used only in XML doc <see cref="..."/>.
// Both legacy (GetUsedAssemblyReferences with doc mode on) and symbol-based paths handle this.
return RunMSBuildAsync(
projectFile: "Library/Library.csproj",
expectedWarnings: [],
useSymbolAnalysis: useSymbolAnalysis);
}

[TestMethod]
[DataRow(true, false)]
[DataRow(false, false)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Dependency
{
public static class Foo
{
public static string Bar() => "Baz";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

</Project>
13 changes: 13 additions & 0 deletions src/Tests/TestData/UsedProjectReferenceCref/Library/Library.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Library
{
public static class Bar
{
/// <summary>
/// See <see cref="Dependency.Foo"/> for details.
/// </summary>
/// <remarks>
/// Also references <see cref="Dependency.Foo.Bar"/>.
/// </remarks>
public static string GetName() => "bar";
}
}
12 changes: 12 additions & 0 deletions src/Tests/TestData/UsedProjectReferenceCref/Library/Library.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../Dependency/Dependency.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Dependency
{
public static class Foo
{
public static string Bar() => "Baz";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Library
{
public static class Bar
{
// Dependency.Foo used only in nameof() — lowered to a string literal in the IOperation tree.
// Only the syntax-level nameof handler catches this.
public static string GetName() => nameof(Dependency.Foo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../Dependency/Dependency.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Dependency
{
public class Foo
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Library
{
public static class Bar
{
// Dependency.Foo used only in switch expression type pattern (ISwitchExpressionArmOperation)
public static string CategorizeExpr(object obj) => obj switch
{
Dependency.Foo => "foo",
_ => "other"
};

// Dependency.Foo used only in switch case clause pattern (IPatternCaseClauseOperation)
public static string CategorizeStmt(object obj)
{
switch (obj)
{
case Dependency.Foo _: return "foo";
default: return "other";
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net472</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../Dependency/Dependency.csproj" />
</ItemGroup>

</Project>