diff --git a/src/Analyzer/ReferenceTrimmerAnalyzer.cs b/src/Analyzer/ReferenceTrimmerAnalyzer.cs index 137b3f4..1979db8 100644 --- a/src/Analyzer/ReferenceTrimmerAnalyzer.cs +++ b/src/Analyzer/ReferenceTrimmerAnalyzer.cs @@ -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; @@ -70,7 +72,7 @@ public override ImmutableArray SupportedDiagnostics public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); context.RegisterCompilationStartAction(CompilationStart); } @@ -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) @@ -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, @@ -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 => { @@ -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 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 usedReferences = new(usedReferencePaths.Keys, StringComparer.OrdinalIgnoreCase); // For bare Reference items (RT0001), we always need a conservative "transitively used" set @@ -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 trackAssembly, + Action 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 — 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 // ────────────────────────────────────────────────────────────────────── diff --git a/src/Tests/E2ETests.cs b/src/Tests/E2ETests.cs index 181567a..9b91efd 100644 --- a/src/Tests/E2ETests.cs +++ b/src/Tests/E2ETests.cs @@ -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 . + // 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)] diff --git a/src/Tests/TestData/UsedProjectReferenceCref/Dependency/Dependency.cs b/src/Tests/TestData/UsedProjectReferenceCref/Dependency/Dependency.cs new file mode 100644 index 0000000..e590b16 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceCref/Dependency/Dependency.cs @@ -0,0 +1,7 @@ +namespace Dependency +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/Tests/TestData/UsedProjectReferenceCref/Dependency/Dependency.csproj b/src/Tests/TestData/UsedProjectReferenceCref/Dependency/Dependency.csproj new file mode 100644 index 0000000..33af96b --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceCref/Dependency/Dependency.csproj @@ -0,0 +1,8 @@ + + + + Library + net472 + + + diff --git a/src/Tests/TestData/UsedProjectReferenceCref/Library/Library.cs b/src/Tests/TestData/UsedProjectReferenceCref/Library/Library.cs new file mode 100644 index 0000000..7d44745 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceCref/Library/Library.cs @@ -0,0 +1,13 @@ +namespace Library +{ + public static class Bar + { + /// + /// See for details. + /// + /// + /// Also references . + /// + public static string GetName() => "bar"; + } +} diff --git a/src/Tests/TestData/UsedProjectReferenceCref/Library/Library.csproj b/src/Tests/TestData/UsedProjectReferenceCref/Library/Library.csproj new file mode 100644 index 0000000..077c6a3 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceCref/Library/Library.csproj @@ -0,0 +1,12 @@ + + + + Library + net472 + + + + + + + diff --git a/src/Tests/TestData/UsedProjectReferenceNameof/Dependency/Dependency.cs b/src/Tests/TestData/UsedProjectReferenceNameof/Dependency/Dependency.cs new file mode 100644 index 0000000..e590b16 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceNameof/Dependency/Dependency.cs @@ -0,0 +1,7 @@ +namespace Dependency +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/Tests/TestData/UsedProjectReferenceNameof/Dependency/Dependency.csproj b/src/Tests/TestData/UsedProjectReferenceNameof/Dependency/Dependency.csproj new file mode 100644 index 0000000..33af96b --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceNameof/Dependency/Dependency.csproj @@ -0,0 +1,8 @@ + + + + Library + net472 + + + diff --git a/src/Tests/TestData/UsedProjectReferenceNameof/Library/Library.cs b/src/Tests/TestData/UsedProjectReferenceNameof/Library/Library.cs new file mode 100644 index 0000000..ec53fa6 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceNameof/Library/Library.cs @@ -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); + } +} diff --git a/src/Tests/TestData/UsedProjectReferenceNameof/Library/Library.csproj b/src/Tests/TestData/UsedProjectReferenceNameof/Library/Library.csproj new file mode 100644 index 0000000..077c6a3 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceNameof/Library/Library.csproj @@ -0,0 +1,12 @@ + + + + Library + net472 + + + + + + + diff --git a/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Dependency/Dependency.cs b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Dependency/Dependency.cs new file mode 100644 index 0000000..d9d54e5 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Dependency/Dependency.cs @@ -0,0 +1,6 @@ +namespace Dependency +{ + public class Foo + { + } +} diff --git a/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Dependency/Dependency.csproj b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Dependency/Dependency.csproj new file mode 100644 index 0000000..33af96b --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Dependency/Dependency.csproj @@ -0,0 +1,8 @@ + + + + Library + net472 + + + diff --git a/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Library/Library.cs b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Library/Library.cs new file mode 100644 index 0000000..a9723f4 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Library/Library.cs @@ -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"; + } + } + } +} diff --git a/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Library/Library.csproj b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Library/Library.csproj new file mode 100644 index 0000000..4309748 --- /dev/null +++ b/src/Tests/TestData/UsedProjectReferenceSwitchPattern/Library/Library.csproj @@ -0,0 +1,13 @@ + + + + Library + net472 + latest + + + + + + +