Skip to content

Commit c5a4ec4

Browse files
authored
Merge pull request #1336 from microsoft/OverloadDisambiguation
Use C# 13 overload resolution attribute to improve friendly overloads
2 parents 52767bc + 50032d4 commit c5a4ec4

10 files changed

+146
-2
lines changed

src/Microsoft.Windows.CsWin32/Generator.Features.cs

+36
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,56 @@ public partial class Generator
1414
private readonly bool canUseUnsafeSkipInit;
1515
private readonly bool canUseUnmanagedCallersOnlyAttribute;
1616
private readonly bool canUseSetLastPInvokeError;
17+
private readonly bool overloadResolutionPriorityAttributePredefined;
1718
private readonly bool unscopedRefAttributePredefined;
1819
private readonly INamedTypeSymbol? runtimeFeatureClass;
1920
private readonly bool generateSupportedOSPlatformAttributes;
2021
private readonly bool generateSupportedOSPlatformAttributesOnInterfaces; // only supported on net6.0 (https://github.com/dotnet/runtime/pull/48838)
2122
private readonly bool generateDefaultDllImportSearchPathsAttribute;
2223
private readonly Dictionary<Feature, bool> supportedFeatures = new();
2324

25+
private void DeclareOverloadResolutionPriorityAttributeIfNecessary()
26+
{
27+
// This attribute may only be applied for C# 13 and later, or else C# errors out.
28+
if (this.LanguageVersion < (LanguageVersion)1300)
29+
{
30+
throw new GenerationFailedException("The OverloadResolutionPriorityAttribute requires C# 13 or later.");
31+
}
32+
33+
if (this.overloadResolutionPriorityAttributePredefined)
34+
{
35+
return;
36+
}
37+
38+
// Always generate these in the context of the most common metadata so we don't emit it more than once.
39+
if (!this.IsWin32Sdk)
40+
{
41+
this.MainGenerator.volatileCode.GenerationTransaction(() => this.MainGenerator.DeclareOverloadResolutionPriorityAttributeIfNecessary());
42+
return;
43+
}
44+
45+
const string name = "OverloadResolutionPriorityAttribute";
46+
this.volatileCode.GenerateSpecialType(name, delegate
47+
{
48+
// This is a polyfill attribute, so never promote visibility to public.
49+
if (!TryFetchTemplate(name, this, out CompilationUnitSyntax? compilationUnit))
50+
{
51+
throw new GenerationFailedException($"Failed to retrieve template: {name}");
52+
}
53+
54+
MemberDeclarationSyntax templateNamespace = compilationUnit.Members.Single();
55+
this.volatileCode.AddSpecialType(name, templateNamespace, topLevel: true);
56+
});
57+
}
58+
2459
private void DeclareUnscopedRefAttributeIfNecessary()
2560
{
2661
if (this.unscopedRefAttributePredefined)
2762
{
2863
return;
2964
}
3065

66+
// Always generate these in the context of the most common metadata so we don't emit it more than once.
3167
if (!this.IsWin32Sdk)
3268
{
3369
this.MainGenerator.volatileCode.GenerationTransaction(() => this.MainGenerator.DeclareUnscopedRefAttributeIfNecessary());

src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs

+7
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,13 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverloads(MethodDefi
630630
friendlyDeclaration = friendlyDeclaration.AddAttributeLists(AttributeList().AddAttributes(supportedOSPlatformAttribute));
631631
}
632632

633+
// If we're using C# 13 or later, consider adding the overload resolution attribute if it would likely resolve ambiguities.
634+
if (this.LanguageVersion >= (LanguageVersion)1300 && parameters.Count == externMethodDeclaration.ParameterList.Parameters.Count)
635+
{
636+
this.DeclareOverloadResolutionPriorityAttributeIfNecessary();
637+
friendlyDeclaration = friendlyDeclaration.AddAttributeLists(AttributeList().AddAttributes(OverloadResolutionPriorityAttribute(1)));
638+
}
639+
633640
friendlyDeclaration = friendlyDeclaration
634641
.WithLeadingTrivia(leadingTrivia);
635642

src/Microsoft.Windows.CsWin32/Generator.Templates.cs

+21
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ private static bool TryFetchTemplate(string name, Generator? generator, [NotNull
3535
}
3636

3737
member = generator?.ElevateVisibility(member) ?? member;
38+
39+
return true;
40+
}
41+
42+
private static bool TryFetchTemplate(string name, Generator? generator, [NotNullWhen(true)] out CompilationUnitSyntax? compilationUnit)
43+
{
44+
string? template = FetchTemplateText(name);
45+
if (template == null)
46+
{
47+
compilationUnit = null;
48+
return false;
49+
}
50+
51+
compilationUnit = SyntaxFactory.ParseCompilationUnit(template, options: generator?.parseOptions) ?? throw new GenerationFailedException($"Unable to parse compilation unit from a template: {name}");
52+
53+
// Strip out #if/#else/#endif trivia, which was already evaluated with the parse options we passed in.
54+
if (generator?.parseOptions is not null)
55+
{
56+
compilationUnit = (CompilationUnitSyntax)compilationUnit.Accept(DirectiveTriviaRemover.Instance)!;
57+
}
58+
3859
return true;
3960
}
4061

src/Microsoft.Windows.CsWin32/Generator.cs

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option
110110
this.canUseUnmanagedCallersOnlyAttribute = this.FindTypeSymbolsIfAlreadyAvailable("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute").Count > 0;
111111
this.canUseSetLastPInvokeError = this.compilation?.GetTypeByMetadataName("System.Runtime.InteropServices.Marshal")?.GetMembers("GetLastSystemError").IsEmpty is false;
112112
this.unscopedRefAttributePredefined = this.FindTypeSymbolIfAlreadyAvailable("System.Diagnostics.CodeAnalysis.UnscopedRefAttribute") is not null;
113+
this.overloadResolutionPriorityAttributePredefined = this.FindTypeSymbolIfAlreadyAvailable("System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute") is not null;
113114
this.runtimeFeatureClass = (INamedTypeSymbol?)this.FindTypeSymbolIfAlreadyAvailable("System.Runtime.CompilerServices.RuntimeFeature");
114115
this.comIIDInterfacePredefined = this.FindTypeSymbolIfAlreadyAvailable($"{this.Namespace}.{IComIIDGuidInterfaceName}") is not null;
115116
this.getDelegateForFunctionPointerGenericExists = this.compilation?.GetTypeByMetadataName(typeof(Marshal).FullName)?.GetMembers(nameof(Marshal.GetDelegateForFunctionPointer)).Any(m => m is IMethodSymbol { IsGenericMethod: true }) is true;

src/Microsoft.Windows.CsWin32/SimpleSyntaxFactory.cs

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ internal static class SimpleSyntaxFactory
111111
internal static readonly IdentifierNameSyntax ComIIDGuidPropertyName = IdentifierName("Guid");
112112
internal static readonly AttributeSyntax FieldOffsetAttributeSyntax = Attribute(IdentifierName("FieldOffset"));
113113

114+
internal static AttributeSyntax OverloadResolutionPriorityAttribute(int priority) => Attribute(ParseName("OverloadResolutionPriority")).AddArgumentListArguments(AttributeArgument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(priority))));
115+
114116
[return: NotNullIfNotNull("marshalAs")]
115117
internal static AttributeSyntax? MarshalAs(MarshalAsAttribute? marshalAs, Generator.NativeArrayInfo? nativeArrayInfo)
116118
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace System.Runtime.CompilerServices
2+
{
3+
/// <summary>
4+
/// Specifies the priority of a member in overload resolution.
5+
/// When unspecified, the default priority is 0.
6+
/// </summary>
7+
[global::System.AttributeUsage(global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Method | global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
8+
internal sealed class OverloadResolutionPriorityAttribute : global::System.Attribute
9+
{
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="OverloadResolutionPriorityAttribute"/> class.
12+
/// </summary>
13+
/// <param name="priority">The priority of the attributed member. Higher numbers are prioritized, lower numbers are deprioritized. 0 is the default if no attribute is present.</param>
14+
public OverloadResolutionPriorityAttribute(int priority)
15+
{
16+
this.Priority = priority;
17+
}
18+
19+
/// <summary>
20+
/// The priority of the member.
21+
/// </summary>
22+
public int Priority { get; }
23+
}
24+
}

test/Microsoft.Windows.CsWin32.Tests/ExternMethodTests.cs

+35
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,41 @@ public void ReferencesToStructWithFlexibleArrayAreAlwaysPointers()
8686
Assert.Empty(this.FindGeneratedType("BITMAPINFO_unmanaged"));
8787
}
8888

89+
[Theory, PairwiseData]
90+
public void OverloadResolutionAttributeUsage(
91+
bool useMatchingLanguageVersion,
92+
[CombinatorialMemberData(nameof(TFMDataNoNetFx35))] string tfm)
93+
{
94+
// Set up the test under the appropriate TFM and either a matching language version or C# 13,
95+
// which is the first version that supports the OverloadResolutionPriorityAttribute.
96+
this.compilation = this.starterCompilations[tfm];
97+
this.parseOptions = this.parseOptions.WithLanguageVersion(
98+
useMatchingLanguageVersion ? (GetLanguageVersionForTfm(tfm) ?? LanguageVersion.Latest) : LanguageVersion.CSharp13);
99+
this.generator = this.CreateGenerator();
100+
101+
this.GenerateApi("EnumDisplayMonitors");
102+
103+
// Emit usage that would be ambiguous without the OverloadResolutionPriorityAttribute.
104+
this.compilation = this.AddCode("""
105+
using Windows.Win32;
106+
using Windows.Win32.Foundation;
107+
using Windows.Win32.Graphics.Gdi;
108+
109+
class Foo
110+
{
111+
static void Use()
112+
{
113+
PInvoke.EnumDisplayMonitors(default, null, default, default);
114+
}
115+
}
116+
""");
117+
118+
Func<Diagnostic, bool>? isAcceptable = this.parseOptions.LanguageVersion >= LanguageVersion.CSharp13
119+
? null // C# 13 and later should not produce any diagnostics.
120+
: diag => diag.Descriptor.Id == "CS0121";
121+
this.AssertNoDiagnostics(this.compilation, logAllGeneratedCode: false, acceptable: isAcceptable);
122+
}
123+
89124
private static AttributeSyntax? FindDllImportAttribute(SyntaxList<AttributeListSyntax> attributeLists) => attributeLists.SelectMany(al => al.Attributes).FirstOrDefault(a => a.Name.ToString() == "DllImport");
90125

91126
private IEnumerable<MethodDeclarationSyntax> GenerateMethod(string methodName)

test/Microsoft.Windows.CsWin32.Tests/GeneratorTestBase.cs

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
public abstract class GeneratorTestBase : IDisposable, IAsyncLifetime
57
{
68
protected const string DefaultTFM = "netstandard2.0";
@@ -215,6 +217,20 @@ protected CSharpCompilation AddGeneratedCode(CSharpCompilation compilation, IGen
215217
return compilation.AddSyntaxTrees(syntaxTrees);
216218
}
217219

220+
/// <summary>
221+
/// Adds a code file to a compilation.
222+
/// </summary>
223+
/// <param name="code">The syntax file to add.</param>
224+
/// <param name="fileName">The name of the code file to add.</param>
225+
/// <param name="compilation">The compilation to add to. When omitted, <see cref="GeneratorTestBase.compilation"/> is assumed.</param>
226+
/// <returns>The modified compilation.</returns>
227+
protected CSharpCompilation AddCode([StringSyntax("c#-test")] string code, string? fileName = null, CSharpCompilation? compilation = null)
228+
{
229+
compilation ??= this.compilation;
230+
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code, this.parseOptions, fileName ?? $"AdditionalCode{compilation.SyntaxTrees.Length + 1}.cs");
231+
return compilation.AddSyntaxTrees(syntaxTree);
232+
}
233+
218234
protected void CollectGeneratedCode(IGenerator generator) => this.compilation = this.AddGeneratedCode(this.compilation, generator);
219235

220236
protected IEnumerable<MethodDeclarationSyntax> FindGeneratedMethod(string name, Compilation? compilation = null) => (compilation ?? this.compilation).SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>()).Where(md => md.Identifier.ValueText == name);
@@ -243,7 +259,7 @@ protected CSharpCompilation AddGeneratedCode(CSharpCompilation compilation, IGen
243259

244260
protected void AssertNoDiagnostics(bool logAllGeneratedCode = true) => this.AssertNoDiagnostics(this.compilation, logAllGeneratedCode);
245261

246-
protected void AssertNoDiagnostics(CSharpCompilation compilation, bool logAllGeneratedCode = true)
262+
protected void AssertNoDiagnostics(CSharpCompilation compilation, bool logAllGeneratedCode = true, Func<Diagnostic, bool>? acceptable = null)
247263
{
248264
var diagnostics = FilterDiagnostics(compilation.GetDiagnostics());
249265
this.logger.WriteLine($"{diagnostics.Length} diagnostics reported.");
@@ -274,7 +290,7 @@ protected void AssertNoDiagnostics(CSharpCompilation compilation, bool logAllGen
274290
}
275291
}
276292

277-
Assert.Empty(diagnostics);
293+
Assert.Empty(acceptable is null ? diagnostics : diagnostics.Where(d => !acceptable(d)));
278294
if (emitSuccessful.HasValue)
279295
{
280296
Assert.Empty(emitDiagnostics);

test/SpellChecker/SpellChecker.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<OutputType>Exe</OutputType>
66
<TargetFrameworks>net8.0-windows8.0;net472</TargetFrameworks>
77
<IsTestProject>false</IsTestProject>
8+
<DisablePolyfill>true</DisablePolyfill>
89
</PropertyGroup>
910

1011
</Project>

test/WinRTInteropTest/WinRTInteropTest.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
77
<Nullable>disable</Nullable>
88
<IsTestProject>false</IsTestProject>
9+
<DisablePolyfill>true</DisablePolyfill>
910
</PropertyGroup>
1011

1112
</Project>

0 commit comments

Comments
 (0)