Skip to content

Add ExcludeStatics support to trimming tools #117904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 23, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,13 @@ internal static bool IsInRequiresScope(this MethodDesc method, string requiresAt
return true;

if (method.OwningType is TypeDesc type && TryGetRequiresAttribute(type, requiresAttribute, out attribute))
return true;
{
if (!ExcludeStatics(attribute.Value))
return true;

if (!method.Signature.IsStatic)
return true;
}

if (method.GetPropertyForAccessor() is PropertyPseudoDesc property && TryGetRequiresAttribute(property, requiresAttribute, out attribute))
return true;
Expand All @@ -123,7 +129,13 @@ internal static bool DoesMethodRequire(this MethodDesc method, string requiresAt

if ((method.Signature.IsStatic || method.IsConstructor) && method.OwningType is TypeDesc owningType &&
!owningType.IsArray && TryGetRequiresAttribute(owningType, requiresAttribute, out attribute))
return true;
{
if (!ExcludeStatics(attribute.Value))
return true;

if (method.IsConstructor)
return true;
}

if (method.GetPropertyForAccessor() is PropertyPseudoDesc @property
&& TryGetRequiresAttribute(@property, requiresAttribute, out attribute))
Expand All @@ -144,7 +156,7 @@ internal static bool DoesFieldRequire(this FieldDesc field, string requiresAttri
return false;
}

return TryGetRequiresAttribute(field.OwningType, requiresAttribute, out attribute);
return TryGetRequiresAttribute(field.OwningType, requiresAttribute, out attribute) && !ExcludeStatics(attribute.Value);
}

internal static bool DoesPropertyRequire(this PropertyPseudoDesc property, string requiresAttribute, [NotNullWhen(returnValue: true)] out CustomAttributeValue<TypeDesc>? attribute) =>
Expand Down Expand Up @@ -174,6 +186,20 @@ internal static bool DoesMemberRequire(this TypeSystemEntity member, string requ
};
}

private static bool ExcludeStatics(CustomAttributeValue<TypeDesc> attribute)
{
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Name == "ExcludeStatics" &&
namedArgument.Value is bool excludeStatics &&
excludeStatics)
{
return true;
}
}
return false;
}

internal const string RequiresUnreferencedCodeAttribute = nameof(RequiresUnreferencedCodeAttribute);
internal const string RequiresDynamicCodeAttribute = nameof(RequiresDynamicCodeAttribute);
internal const string RequiresAssemblyFilesAttribute = nameof(RequiresAssemblyFilesAttribute);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class LinkedMethodEntity : LinkedEntity
"<Module>.MainMethodWrapper()",
"<Module>.MainMethodWrapper(String[])",
"System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute.__GetFieldHelper(Int32,MethodTable*&)",
"System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.__GetFieldHelper(Int32,MethodTable*&)",
"System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.__GetFieldHelper(Int32,MethodTable*&)",
"System.Runtime.InteropServices.TypeMapping",
"System.Runtime.InteropServices.TypeMapping.GetOrCreateExternalTypeMapping<TTypeMapGroup>()",
"System.Runtime.InteropServices.TypeMapping.GetOrCreateProxyTypeMapping<TTypeMapGroup>()",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ public IEnumerable<NPath> GetCommonSourceFiles()
.Combine("Support")
.Combine("DynamicallyAccessedMembersAttribute.cs");
yield return dam;

var sharedDir = _testCase.RootCasesDirectory.Parent.Parent
.Combine("src")
.Combine("ILLink.Shared");
yield return sharedDir.Combine("RequiresDynamicCodeAttribute.cs");
yield return sharedDir.Combine("RequiresUnreferencedCodeAttribute.cs");
}

public virtual IEnumerable<string> GetCommonReferencedAssemblies(NPath workingDirectory)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ public static bool DoesMemberRequire(this ISymbol member, string requiresAttribu
return true;

// Also check the containing type
if (member.IsStatic || member.IsConstructor())
return member.ContainingType.TryGetAttribute(requiresAttribute, out requiresAttributeData);
if ((member.IsStatic || member.IsConstructor()) && member.ContainingType.TryGetAttribute(requiresAttribute, out requiresAttributeData))
{
if (!ExcludeStatics(requiresAttributeData))
return true;

if (member.IsConstructor())
return true;
}

return false;
}
Expand All @@ -33,6 +39,18 @@ public static bool IsInRequiresScope(this ISymbol member, string attributeName)
return member.IsInRequiresScope(attributeName, out _);
}

private static bool ExcludeStatics(AttributeData attributeData)
{
foreach (var namedArg in attributeData.NamedArguments)
{
if (namedArg.Key == "ExcludeStatics" && namedArg.Value.Value is bool b)
{
return b;
}
}
return false;
}

// TODO: Consider sharing with ILLink IsInRequiresScope method
/// <summary>
/// True if the source of a call is considered to be annotated with the Requires... attribute
Expand All @@ -58,7 +76,13 @@ public static bool IsInRequiresScope(this ISymbol member, string attributeName,
}

if (member.ContainingType is ITypeSymbol containingType && containingType.TryGetAttribute(attributeName, out requiresAttribute))
return true;
{
if (!ExcludeStatics(requiresAttribute))
return true;

if (!member.IsStatic)
return true;
}

if (member is IMethodSymbol { AssociatedSymbol: { } associated } && associated.TryGetAttribute(attributeName, out requiresAttribute))
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if INCLUDE_EXPECTATIONS
using Mono.Linker.Tests.Cases.Expectations.Assertions;
#endif

#nullable enable

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Indicates that the specified method requires the ability to generate new code at runtime,
/// for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when compiling ahead of time.
/// </remarks>
#if INCLUDE_EXPECTATIONS
[SkipKeptItemsValidation]
#endif
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
#if SYSTEM_PRIVATE_CORELIB
public
#else
internal
#endif
sealed class RequiresDynamicCodeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresDynamicCodeAttribute"/> class
/// with the specified message.
/// </summary>
/// <param name="message">
/// A message that contains information about the usage of dynamic code.
/// </param>
public RequiresDynamicCodeAttribute(string message)
{
Message = message;
}

/// <summary>
/// Indicates whether the attribute should apply to static members.
/// </summary>
public bool ExcludeStatics { get; set; }

/// <summary>
/// Gets a message that contains information about the usage of dynamic code.
/// </summary>
public string Message { get; }

/// <summary>
/// Gets or sets an optional URL that contains more information about the method,
/// why it requires dynamic code, and what options a consumer has to deal with it.
/// </summary>
public string? Url { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if INCLUDE_EXPECTATIONS
using Mono.Linker.Tests.Cases.Expectations.Assertions;
#endif

#nullable enable

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Indicates that the specified method requires dynamic access to code that is not referenced
/// statically, for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when removing unreferenced
/// code from an application.
/// </remarks>
#if INCLUDE_EXPECTATIONS
[SkipKeptItemsValidation]
#endif
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
#if SYSTEM_PRIVATE_CORELIB
public
#else
internal
#endif
sealed class RequiresUnreferencedCodeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresUnreferencedCodeAttribute"/> class
/// with the specified message.
/// </summary>
/// <param name="message">
/// A message that contains information about the usage of unreferenced code.
/// </param>
public RequiresUnreferencedCodeAttribute(string message)
{
Message = message;
}

/// <summary>
/// Indicates whether the attribute should apply to static members.
/// </summary>
public bool ExcludeStatics { get; set; }

/// <summary>
/// Gets a message that contains information about the usage of unreferenced code.
/// </summary>
public string Message { get; }

/// <summary>
/// Gets or sets an optional URL that contains more information about the method,
/// why it requires unreferenced code, and what options a consumer has to deal with it.
/// </summary>
public string? Url { get; set; }
}
}
18 changes: 15 additions & 3 deletions src/tools/illink/src/linker/Linker/Annotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,13 @@ internal bool IsInRequiresUnreferencedCodeScope(MethodDefinition method, [NotNul
return true;

if (method.DeclaringType is not null && TryGetLinkerAttribute(method.DeclaringType, out attribute))
return true;
{
if (!attribute.ExcludeStatics)
return true;

if (!method.IsStatic)
return true;
}

attribute = null;
return false;
Expand Down Expand Up @@ -676,7 +682,13 @@ internal bool DoesMethodRequireUnreferencedCode(MethodDefinition originalMethod,

if ((method.IsStatic || method.IsConstructor) && method.DeclaringType is not null &&
TryGetLinkerAttribute(method.DeclaringType, out attribute))
return true;
{
if (!attribute.ExcludeStatics)
return true;

if (method.IsConstructor)
return true;
}
} while (context.CompilerGeneratedState.TryGetOwningMethodForCompilerGeneratedMember(method, out method));

attribute = null;
Expand All @@ -691,7 +703,7 @@ internal bool DoesFieldRequireUnreferencedCode(FieldDefinition field, [NotNullWh
return false;
}

return TryGetLinkerAttribute(field.DeclaringType, out attribute);
return TryGetLinkerAttribute(field.DeclaringType, out attribute) && !attribute.ExcludeStatics;
}

/// <Summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,28 @@ public IEnumerable<T> GetAttributes<T>() where T : Attribute

if (customAttribute.HasConstructorArguments && customAttribute.ConstructorArguments[0].Value is string message)
{
var ruca = new RequiresUnreferencedCodeAttribute(message);
string? url = null;
bool excludeStatics = false;
if (customAttribute.HasProperties)
{
foreach (var prop in customAttribute.Properties)
{
if (prop.Name == "Url")
{
ruca.Url = prop.Argument.Value as string;
break;
url = prop.Argument.Value as string;
}
else if (prop.Name == "ExcludeStatics" && prop.Argument.Value is true)
{
excludeStatics = true;
}
}
}

return ruca;
return new RequiresUnreferencedCodeAttribute(message)
{
Url = url,
ExcludeStatics = excludeStatics
};
}

context.LogWarning((IMemberDefinition)provider, DiagnosticId.AttributeDoesntHaveTheRequiredNumberOfParameters, typeof(RequiresUnreferencedCodeAttribute).FullName ?? "");
Expand Down
2 changes: 2 additions & 0 deletions src/tools/illink/src/linker/Mono.Linker.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<RollForward>Major</RollForward>
<UseAppHost>false</UseAppHost>
<NoWarn>$(NoWarn);NU5131</NoWarn>
<!-- Allow overriding RequiresUnreferencedCodeAttribute -->
<NoWarn>$(NoWarn);CS0436</NoWarn>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_AddReferenceAssemblyToPackage</TargetsForTfmSpecificContentInPackage>
<DefineConstants>$(DefineConstants);ILLINK</DefineConstants>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public Task RequiresAttributeMismatch()
return RunTest(nameof(RequiresAttributeMismatch));
}

[Fact]
public Task RequiresExcludeStatics()
{
return RunTest();
}

[Fact]
public Task RequiresCapabilityFromCopiedAssembly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,20 @@ public static (CompilationWithAnalyzers Compilation, SemanticModel SemanticModel
var sources = new List<SyntaxTree>() { src };
sources.AddRange(additionalSources ?? Array.Empty<SyntaxTree>());
TestCaseUtils.GetDirectoryPaths(out string rootSourceDirectory);
var commonSourcePath = Path.Combine(Path.GetDirectoryName(rootSourceDirectory)!,
"Mono.Linker.Tests.Cases.Expectations",
"Support",
"DynamicallyAccessedMembersAttribute.cs");
sources.Add(CSharpSyntaxTree.ParseText(File.ReadAllText(commonSourcePath), path: commonSourcePath));
var testDir = Path.GetDirectoryName(rootSourceDirectory)!;
var srcDir = Path.Combine(Path.GetDirectoryName(testDir)!, "src");
var sharedDir = Path.Combine(srcDir, "ILLink.Shared");
var commonSourcePaths = new List<string>()
{
Path.Combine(testDir,
"Mono.Linker.Tests.Cases.Expectations",
"Support",
"DynamicallyAccessedMembersAttribute.cs"),
Path.Combine(sharedDir, "RequiresUnreferencedCodeAttribute.cs"),
Path.Combine(sharedDir, "RequiresDynamicCodeAttribute.cs"),
};

sources.AddRange(commonSourcePaths.Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), path: p)));
var comp = CSharpCompilation.Create(
assemblyName: Guid.NewGuid().ToString("N"),
syntaxTrees: sources,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static async Task RunTestFile(string suiteName, string testName, bool all
testCaseDir = testSuiteDir;
testPath = Path.Combine(testSuiteDir, $"{testName}.cs");
}
Assert.True(File.Exists(testPath));
Assert.True(File.Exists(testPath), $"{testPath} should exist");
var tree = SyntaxFactory.ParseSyntaxTree(
SourceText.From(File.OpenRead(testPath), Encoding.UTF8),
path: testPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\ILLink.Shared\DynamicallyAccessedMemberTypesEx.cs" Link="Support\DynamicallyAccessedMemberTypesEx.cs" />
<Compile Include="..\..\src\ILLink.Shared\RequiresDynamicCodeAttribute.cs" Link="Support\RequiresDynamicCodeAttribute.cs" />
<Compile Include="..\..\src\ILLink.Shared\RequiresUnreferencedCodeAttribute.cs" Link="Support\RequiresUnreferencedCodeAttribute.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@

<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)../Mono.Linker.Tests.Cases.Expectations/Support/DynamicallyAccessedMembersAttribute.cs" Link="Support\DynamicallyAccessedMembersAttribute.cs" />
<Compile Include="..\..\src\ILLink.Shared\RequiresDynamicCodeAttribute.cs" Link="Support\RequiresDynamicCodeAttribute.cs" />
<Compile Include="..\..\src\ILLink.Shared\RequiresUnreferencedCodeAttribute.cs" Link="Support\RequiresUnreferencedCodeAttribute.cs" />
</ItemGroup>

</Project>
Loading
Loading