diff --git a/src/CsWin32Generator/Program.cs b/src/CsWin32Generator/Program.cs index 6d4b0191..82a3e80e 100644 --- a/src/CsWin32Generator/Program.cs +++ b/src/CsWin32Generator/Program.cs @@ -23,6 +23,7 @@ public partial class Program private bool verbose; private string? assemblyName; private FileInfo? assemblyOriginatorKeyFile; + private LanguageVersion languageVersion = LanguageVersion.CSharp13; /// /// Initializes a new instance of the class. @@ -118,6 +119,11 @@ public async Task Main( Description = "Path to the strong name key file (.snk) for signing.", }; + var languageVersionOption = new Option("--language-version") + { + Description = "C# language version (e.g., 10, 11, 12, 13, Latest, Preview).", + }; + var verboseOption = new Option("--verbose"); var rootCommand = new RootCommand("CsWin32 Code Generator - Generates P/Invoke methods and supporting types from Windows metadata.") @@ -133,6 +139,7 @@ public async Task Main( referencesOption, assemblyNameOption, keyFileOption, + languageVersionOption, verboseOption, }; @@ -149,6 +156,7 @@ public async Task Main( this.assemblyName = parseResult.GetValue(assemblyNameOption); this.assemblyOriginatorKeyFile = parseResult.GetValue(keyFileOption); this.verbose = parseResult.GetValue(verboseOption); + string? languageVersionString = parseResult.GetValue(languageVersionOption); // Check for errors before continuing. if (parseResult.Errors.Count > 0) @@ -161,6 +169,15 @@ public async Task Main( return 1; } + if (languageVersionString is not null) + { + if (!LanguageVersionFacts.TryParse(languageVersionString, out this.languageVersion)) + { + this.ReportError($"Invalid language version: {languageVersionString}"); + return 1; + } + } + try { var result = await this.GenerateCode( @@ -261,7 +278,7 @@ private Task GenerateCode( // Create compilation context CSharpCompilation? compilation = this.CreateCompilation(allowUnsafeBlocks: true, platform, references); - CSharpParseOptions? parseOptions = this.CreateParseOptions(targetFramework); + CSharpParseOptions? parseOptions = this.CreateParseOptions(targetFramework)?.WithLanguageVersion(this.languageVersion); this.VerboseWriteLine($"Created compilation context with platform: {platform}, language version: {parseOptions?.LanguageVersion}"); // Load docs if available diff --git a/src/Microsoft.Windows.CsWin32.BuildTasks/CsWin32CodeGeneratorTask.cs b/src/Microsoft.Windows.CsWin32.BuildTasks/CsWin32CodeGeneratorTask.cs index db10312c..c6958271 100644 --- a/src/Microsoft.Windows.CsWin32.BuildTasks/CsWin32CodeGeneratorTask.cs +++ b/src/Microsoft.Windows.CsWin32.BuildTasks/CsWin32CodeGeneratorTask.cs @@ -104,6 +104,11 @@ public CsWin32CodeGeneratorTask(IToolExecutor? toolExecutor) /// public string? KeyFile { get; set; } + /// + /// Gets or sets the C# language version (e.g., 10, 11, 12, 13, Latest, Preview). + /// + public string? LangVersion { get; set; } + /// /// Gets the generated source files. /// @@ -191,6 +196,7 @@ protected override string GenerateResponseFileCommands() commandLine.AppendSwitchIfNotNull("--platform ", this.Platform); commandLine.AppendSwitchIfNotNull("--assembly-name ", this.AssemblyName); commandLine.AppendSwitchIfNotNull("--key-file ", this.KeyFile); + commandLine.AppendSwitchIfNotNull("--language-version ", this.LangVersion); commandLine.AppendSwitch("--verbose "); if (this.References?.Length > 0) diff --git a/src/Microsoft.Windows.CsWin32/Generator.Features.cs b/src/Microsoft.Windows.CsWin32/Generator.Features.cs index 333ffaf7..311fca2f 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.Features.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.Features.cs @@ -20,6 +20,7 @@ public partial class Generator private readonly bool canMarshalNativeDelegateParams; private readonly bool overloadResolutionPriorityAttributePredefined; private readonly bool unscopedRefAttributePredefined; + private readonly bool canUseComVariant; private readonly INamedTypeSymbol? runtimeFeatureClass; private readonly bool generateSupportedOSPlatformAttributes; private readonly bool generateSupportedOSPlatformAttributesOnInterfaces; // only supported on net6.0 (https://github.com/dotnet/runtime/pull/48838) @@ -30,6 +31,8 @@ public partial class Generator internal bool CanUseIPropertyValue => this.canUseIPropertyValue; + internal bool CanUseComVariant => this.canUseComVariant; + private void DeclareOverloadResolutionPriorityAttributeIfNecessary() { // This attribute may only be applied for C# 13 and later, or else C# errors out. diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 1e3f6c13..d12900cd 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -124,6 +124,7 @@ public Generator(string metadataLibraryPath, Docs? docs, IEnumerable add this.getDelegateForFunctionPointerGenericExists = this.compilation?.GetTypeByMetadataName(typeof(Marshal).FullName)?.GetMembers(nameof(Marshal.GetDelegateForFunctionPointer)).Any(m => m is IMethodSymbol { IsGenericMethod: true }) is true; this.generateDefaultDllImportSearchPathsAttribute = this.compilation?.GetTypeByMetadataName(typeof(DefaultDllImportSearchPathsAttribute).FullName) is object; this.canUseIPropertyValue = this.compilation?.GetTypeByMetadataName("Windows.Foundation.IPropertyValue")?.DeclaredAccessibility == Accessibility.Public; + this.canUseComVariant = this.compilation?.GetTypeByMetadataName("System.Runtime.InteropServices.Marshalling.ComVariant") is not null; if (this.FindTypeSymbolIfAlreadyAvailable("System.Runtime.Versioning.SupportedOSPlatformAttribute") is { } attribute) { this.generateSupportedOSPlatformAttributes = true; diff --git a/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs b/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs index bd95fb82..0d39caee 100644 --- a/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs +++ b/src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs @@ -159,7 +159,7 @@ internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs return new TypeSyntaxAndMarshaling(IdentifierName(specialName)); } } - else if (useComSourceGenerators && simpleName is "VARIANT") + else if (useComSourceGenerators && simpleName is "VARIANT" && this.Generator.CanUseComVariant) { return new TypeSyntaxAndMarshaling(QualifiedName(ParseName("global::System.Runtime.InteropServices.Marshalling"), IdentifierName("ComVariant"))); } @@ -311,8 +311,13 @@ private static bool TryMarshalAsObject(TypeSyntaxSettings inputs, string name, [ marshalAs = new MarshalAsAttribute(UnmanagedType.IDispatch); return true; case "VARIANT": - marshalAs = new MarshalAsAttribute(UnmanagedType.Struct); - return true; + if (inputs.Generator?.UseSourceGenerators != true) + { + marshalAs = new MarshalAsAttribute(UnmanagedType.Struct); + return true; + } + + break; } } diff --git a/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.targets b/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.targets index 432961d0..b28d755e 100644 --- a/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.targets +++ b/src/Microsoft.Windows.CsWin32/build/Microsoft.Windows.CsWin32.targets @@ -67,6 +67,7 @@ GeneratorToolPath="$(CsWin32GeneratorToolPath)CsWin32Generator.dll" TargetFramework="$(TargetFramework)" Platform="$(Platform)" + LangVersion="$(LangVersion)" References="@(ReferencePath)" ContinueOnError="$(CsWin32ContinueOnError)" Condition="@(CsWin32NativeMethodsTxt->Count()) > 0"> diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorFullTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorFullTests.cs index bd94c19d..1a137583 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorFullTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorFullTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace CsWin32Generator.Tests; @@ -12,11 +13,15 @@ public CsWin32GeneratorFullTests(ITestOutputHelper logger) { } - [Fact] + [Theory] [Trait("TestCategory", "FailsInCloudTest")] // these take ~4GB of memory to run. - public async Task FullGeneration() + [InlineData("net8.0", LanguageVersion.CSharp12)] + [InlineData("net9.0", LanguageVersion.CSharp13)] + public async Task FullGeneration(string tfm, LanguageVersion langVersion) { this.fullGeneration = true; - await this.InvokeGeneratorAndCompile(TestOptions.None, "FullGeneration"); + this.compilation = this.starterCompilations[tfm]; + this.parseOptions = this.parseOptions.WithLanguageVersion(langVersion); + await this.InvokeGeneratorAndCompile($"FullGeneration_{tfm}_{langVersion}", TestOptions.None); } } diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs index cf367f1d..bc777caa 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#pragma warning disable SA1402 +#pragma warning disable SA1402,SA1201,SA1202 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Xunit; namespace CsWin32Generator.Tests; @@ -21,7 +22,7 @@ public async Task TestGenerateIDispatch() { // IDispatch is not normally emitted, but we need it for source generated com so check that it got generated. this.nativeMethods.Add("IDispatch"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); var idispatchType = this.FindGeneratedType("IDispatch"); Assert.NotEmpty(idispatchType); @@ -31,7 +32,7 @@ public async Task TestGenerateIDispatch() public async Task TestGenerateIShellWindows() { this.nativeMethods.Add("IShellWindows"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); var ishellWindowsType = this.FindGeneratedType("IShellWindows"); Assert.NotEmpty(ishellWindowsType); @@ -44,7 +45,7 @@ public async Task TestGenerateIShellWindows() public async Task TestNativeMethods() { this.nativeMethodsTxt = "NativeMethods.txt"; - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); } [Fact] @@ -52,7 +53,7 @@ public async Task CheckITypeCompIsUnmanaged() { // Request DebugPropertyInfo and we should see ITypeComp_unmanaged get generated because it has an embedded managed field this.nativeMethods.Add("DebugPropertyInfo"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); var iface = this.FindGeneratedType("ITypeComp_unmanaged"); Assert.True(iface.Any()); @@ -63,7 +64,7 @@ public async Task CheckIAudioProcessingObjectConfigurationDoesNotGenerateUnmanag { // Request IAudioProcessingObjectConfiguration and it should request IAudioMediaType_unmanaged that's embedded in a struct this.nativeMethods.Add("IAudioProcessingObjectConfiguration"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); var iface = this.FindGeneratedType("IAudioMediaType_unmanaged"); Assert.True(iface.Any()); @@ -75,7 +76,7 @@ public async Task TestGenerateIUnknownAndID3D11DeviceContext() // If IUnknown is requested first and then it's needed as an unmanaged type, we fail to generate it. this.nativeMethods.Add("IUnknown"); this.nativeMethods.Add("ID3D11DeviceContext"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); } [Fact] @@ -84,7 +85,7 @@ public async Task TestGenerateSomethingInWin32System() // If we need CharSet _and_ we generate something in Windows.Win32.System, the partially qualified reference breaks. this.nativeMethods.Add("GetDistanceOfClosestLanguageInList"); this.nativeMethods.Add("ADVANCED_FEATURE_FLAGS"); - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); } [Theory] @@ -94,67 +95,87 @@ public async Task VerifySignature(string api, string member, string signature) { // If we need CharSet _and_ we generate something in Windows.Win32.System, the partially qualified reference breaks. this.nativeMethods.Add(api); - await this.InvokeGeneratorAndCompile(TestOptions.None, $"{api}_{member}"); + await this.InvokeGeneratorAndCompile($"{api}_{member}"); var generatedMemberSignatures = this.FindGeneratedMethod(member).Select(x => x.ParameterList.ToString()); Assert.Contains($"({signature})", generatedMemberSignatures); } + public static IList TestApiData => [ + ["CHAR", "Simple type"], + ["RmRegisterResources", "Simple function"], + ["IStream", "Interface with enum parameters that need to be marshaled as U4"], + ["IServiceProvider", "exercises out parameter of type IUnknown marshaled to object"], + ["CreateDispatcherQueueController", "Has WinRT object parameter which needs marshalling"], + ["IEnumEventObject", "Derives from IDispatch"], + ["DestroyIcon", "Exercise SetLastError on import"], + ["IDebugProperty", "DebugPropertyInfo has a managed field"], + ["GetThemeColor", "Tests enum marshaling to I4"], + ["ChoosePixelFormat", "Tests marshaled field in struct"], + ["IGraphicsEffectD2D1Interop", "Uses Uses IPropertyValue (not accessible in C#]"], + ["Folder3", "Derives from multiple interfaces"], + ["IAudioProcessingObjectConfiguration", "Test struct** parameter marshalling"], + ["IBDA_EasMessage", "[In,[Out, should not be added"], + ["ITypeComp", "ITypeComp should not be unmanaged"], + ["AsyncIConnectedIdentityProvider", "Needs [In,[Out, on array parameter"], + ["Column", "SYSLIB1092 that needs to be suppressed"], + ["IBidiAsyncNotifyChannel", "Parameter that needs special marshaling help"], + ["ID3D11Texture1D", "Unmanaged interface needs to not use `out` for any params"], + ["ID3D11DeviceContext", "ppClassInstances parameter is annotated to have a size from uint* parameter"], + ["IBrowserService2", "Array return type needs marshaling attribute"], + ["ID3D11VideoContext", "Parameter not properly annotated as marshaled"], + ["ID2D1Factory4", "Function matches more than one interface member. Which interface member is actually chosen is implementation-dependent. Consider using a non-explicit implementation instead"], + ["IDWriteFontFace5", "Pointers may only be used in an unsafe context"], + ["ICorProfilerCallback11", "already defines a member called 'SurvivingReferences' with the same parameter types"], + ["D3D11_VIDEO_DECODER_BUFFER_DESC1", "struct with pointer to struct"], + ["D3D11_VIDEO_DECODER_EXTENSION", "struct with array of managed types"], + ["IWICBitmap", "Interface with multiple methods with same name"], + ["ID3D12VideoDecodeCommandList1", "D3D12_VIDEO_DECODE_OUTPUT_STREAM_ARGUMENTS1 has an inline fixed length array of managed structs"], + ["IDebugHostContext", "Marshaling bool without explicit marshaling information is not allowed"], + ["HlinkCreateFromData", "IDataObject is not supported by source-generated COM"], + ["IVPNotify", "IVPNotify derives from an interface that's missing a GUID"], + ["PathGetCharType", "CharSet forwarded to another assembly"], + ["ItsPubPlugin", "The platform 'windowsserver' is not a known platform name"], + ["PFIND_MATCHING_THREAD", "Delegates can't have marshaling", TestOptions.GeneratesNothing], + ["LPFNDFMCALLBACK", "Delegate return value can't be marshaled", TestOptions.GeneratesNothing], + ["IDataObject", "Source generated COM can't rely on built-in IDataObject so make sure IDataObject generates correctly"], + ["JsCreateRuntime", "Delegate has bool parameter which should not get MarshalAs(bool] added to it in blittable mode"], + ["RoCreatePropertySetSerializer", "IPropertySetSerializer parameter doesn't get interface generated for it."], + ["D3D9ON12_ARGS", "D3D9ON12_ARGS has an inline object[2, array in a struct"], + ["Direct3DCreate9On12Ex", "D3D9ON12_ARGS argument is not supported for marshalling"], + ["IDWriteGdiInterop1", "InterfaceImplementation already declares ABI_GetFontSignature"], + ["IMFSinkWriterEx", "Introducing a 'Finalize' method can interfere with destructor invocation. Did you intend to declare a destructor?"], + ["RoRegisterActivationFactories", "The type 'delegate* unmanaged[Stdcall,' may not be used as a type argument"], + ["IMFMediaKeys", "cannot convert from 'Windows.Win32.Foundation.BSTR*' to 'object'"], + ["ICompositorInterop2", "Needs type from UAP contract that isn't available"], + ["SECURITY_NULL_SID_AUTHORITY", "static struct with embedded array incorrectly initialized"], + ["CreateThreadpoolWork", "Friendly overload differs only on return type and 'in' modifiers on attributes"], + ["GetModuleFileName", "Should have a friendly Span overload"], + ]; + [Theory] - [InlineData("CHAR", "Simple type")] - [InlineData("RmRegisterResources", "Simple function")] - [InlineData("IStream", "Interface with enum parameters that need to be marshaled as U4")] - [InlineData("IServiceProvider", "exercises out parameter of type IUnknown marshaled to object")] - [InlineData("CreateDispatcherQueueController", "Has WinRT object parameter which needs marshalling")] - [InlineData("IEnumEventObject", "Derives from IDispatch")] - [InlineData("DestroyIcon", "Exercise SetLastError on import")] - [InlineData("IDebugProperty", "DebugPropertyInfo has a managed field")] - [InlineData("GetThemeColor", "Tests enum marshaling to I4")] - [InlineData("ChoosePixelFormat", "Tests marshaled field in struct")] - [InlineData("IGraphicsEffectD2D1Interop", "Uses Uses IPropertyValue (not accessible in C#)")] - [InlineData("Folder3", "Derives from multiple interfaces")] - [InlineData("IAudioProcessingObjectConfiguration", "Test struct** parameter marshalling")] - [InlineData("IBDA_EasMessage", "[In][Out] should not be added")] - [InlineData("ITypeComp", "ITypeComp should not be unmanaged")] - [InlineData("AsyncIConnectedIdentityProvider", "Needs [In][Out] on array parameter")] - [InlineData("Column", "SYSLIB1092 that needs to be suppressed")] - [InlineData("IBidiAsyncNotifyChannel", "Parameter that needs special marshaling help")] - [InlineData("ID3D11Texture1D", "Unmanaged interface needs to not use `out` for any params")] - [InlineData("ID3D11DeviceContext", "ppClassInstances parameter is annotated to have a size from uint* parameter")] - [InlineData("IBrowserService2", "Array return type needs marshaling attribute")] - [InlineData("ID3D11VideoContext", "Parameter not properly annotated as marshaled")] - [InlineData("ID2D1Factory4", "Function matches more than one interface member. Which interface member is actually chosen is implementation-dependent. Consider using a non-explicit implementation instead")] - [InlineData("IDWriteFontFace5", "Pointers may only be used in an unsafe context")] - [InlineData("ICorProfilerCallback11", "already defines a member called 'SurvivingReferences' with the same parameter types")] - [InlineData("D3D11_VIDEO_DECODER_BUFFER_DESC1", "struct with pointer to struct")] - [InlineData("D3D11_VIDEO_DECODER_EXTENSION", "struct with array of managed types")] - [InlineData("IWICBitmap", "Interface with multiple methods with same name")] - [InlineData("ID3D12VideoDecodeCommandList1", "D3D12_VIDEO_DECODE_OUTPUT_STREAM_ARGUMENTS1 has an inline fixed length array of managed structs")] - [InlineData("IDebugHostContext", "Marshaling bool without explicit marshaling information is not allowed")] - [InlineData("HlinkCreateFromData", "IDataObject is not supported by source-generated COM")] - [InlineData("IVPNotify", "IVPNotify derives from an interface that's missing a GUID")] - [InlineData("PathGetCharType", "CharSet forwarded to another assembly")] - [InlineData("ItsPubPlugin", "The platform 'windowsserver' is not a known platform name")] - [InlineData("PFIND_MATCHING_THREAD", "Delegates can't have marshaling", TestOptions.GeneratesNothing)] - [InlineData("LPFNDFMCALLBACK", "Delegate return value can't be marshaled", TestOptions.GeneratesNothing)] - [InlineData("IDataObject", "Source generated COM can't rely on built-in IDataObject so make sure IDataObject generates correctly")] - [InlineData("JsCreateRuntime", "Delegate has bool parameter which should not get MarshalAs(bool) added to it in blittable mode")] - [InlineData("RoCreatePropertySetSerializer", "IPropertySetSerializer parameter doesn't get interface generated for it.")] - [InlineData("D3D9ON12_ARGS", "D3D9ON12_ARGS has an inline object[2] array in a struct")] - [InlineData("Direct3DCreate9On12Ex", "D3D9ON12_ARGS argument is not supported for marshalling")] - [InlineData("IDWriteGdiInterop1", "InterfaceImplementation already declares ABI_GetFontSignature")] - [InlineData("IMFSinkWriterEx", "Introducing a 'Finalize' method can interfere with destructor invocation. Did you intend to declare a destructor?")] - [InlineData("RoRegisterActivationFactories", "The type 'delegate* unmanaged[Stdcall]' may not be used as a type argument")] - [InlineData("IMFMediaKeys", "cannot convert from 'Windows.Win32.Foundation.BSTR*' to 'object'")] - [InlineData("ICompositorInterop2", "Needs type from UAP contract that isn't available")] - [InlineData("SECURITY_NULL_SID_AUTHORITY", "static struct with embedded array incorrectly initialized")] - [InlineData("CreateThreadpoolWork", "Friendly overload differs only on return type and 'in' modifiers on attributes")] - [InlineData("GetModuleFileName", "Should have a friendly Span overload")] + [MemberData(nameof(TestApiData))] public async Task TestGenerateApi(string api, string purpose, TestOptions options = TestOptions.None) { - this.Logger.WriteLine($"Testing {api} - {purpose}"); + await this.TestGenerateApiWorker(api, purpose, options, "net9.0"); + } + + [Theory] + [MemberData(nameof(TestApiData))] + public async Task TestGenerateApiNet8(string api, string purpose, TestOptions options = TestOptions.None) + { + await this.TestGenerateApiWorker(api, purpose, options, "net8.0"); + } + + private async Task TestGenerateApiWorker(string api, string purpose, TestOptions options, string tfm) + { + LanguageVersion langVersion = (tfm == "net8.0") ? LanguageVersion.CSharp12 : LanguageVersion.CSharp13; + + this.compilation = this.starterCompilations[tfm]; + this.parseOptions = this.parseOptions.WithLanguageVersion(langVersion); + this.Logger.WriteLine($"Testing {api} - {tfm} - {purpose}"); this.nativeMethods.Add(api); - await this.InvokeGeneratorAndCompile(options, $"Test_{api}"); + await this.InvokeGeneratorAndCompile($"Test_{api}_{tfm}", options); } [Fact] @@ -184,7 +205,7 @@ public async Task GenerateCommandLineCommands_WithNativeMethodsJsonContainingCom this.nativeMethods.Add("IUnknown"); // Add a method to ensure generation occurs // Act - await this.InvokeGeneratorAndCompile(); + await this.InvokeGeneratorAndCompileFromFact(); } [Theory] @@ -210,7 +231,7 @@ public async Task TestNativeMethodsExclusion(string scenario, string[] includes, } // Invoke the generator and compile - await this.InvokeGeneratorAndCompile(TestOptions.GeneratesNothing | TestOptions.DoNotFailOnDiagnostics, $"TestNativeMethodsExclusion_{scenario}"); + await this.InvokeGeneratorAndCompile($"TestNativeMethodsExclusion_{scenario}", TestOptions.GeneratesNothing | TestOptions.DoNotFailOnDiagnostics); // Verify the results based on includes and excludes Assert.Empty(this.FindGeneratedType(checkNotPresent)); @@ -284,4 +305,32 @@ internal struct PCWSTR File.Delete(referencedAssemblyPath); } + + // https://github.com/microsoft/CsWin32/issues/1494 + [Theory] + [InlineData(LanguageVersion.CSharp12)] + [InlineData(LanguageVersion.CSharp13)] + public async Task VerifyOverloadPriorityAttributeInNet8(LanguageVersion langVersion) + { + this.compilation = this.starterCompilations["net8.0"]; + this.parseOptions = this.parseOptions.WithLanguageVersion(langVersion); + this.nativeMethods.Add("IShellLinkW"); + await this.InvokeGeneratorAndCompile($"{nameof(this.VerifyOverloadPriorityAttributeInNet8)}_{langVersion}"); + + // See if we generated any methods with OverloadResolutionPriorityAttribute. + var methods = this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType()); + var methodsWithAttribute = methods + .Where(md => FindAttribute(md.AttributeLists, "OverloadResolutionPriority").Any()); + + IEnumerable FindAttribute(SyntaxList attributeLists, string name) => attributeLists.SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == name); + + if (langVersion >= LanguageVersion.CSharp13) + { + Assert.NotEmpty(methodsWithAttribute); + } + else + { + Assert.Empty(methodsWithAttribute); + } + } } diff --git a/test/CsWin32Generator.Tests/CsWin32GeneratorTestsBase.cs b/test/CsWin32Generator.Tests/CsWin32GeneratorTestsBase.cs index c6aaaf65..2e9c99c2 100644 --- a/test/CsWin32Generator.Tests/CsWin32GeneratorTestsBase.cs +++ b/test/CsWin32Generator.Tests/CsWin32GeneratorTestsBase.cs @@ -44,7 +44,13 @@ public override async ValueTask InitializeAsync() this.compilation = this.starterCompilations["net9.0"]; } - protected async Task InvokeGeneratorAndCompile(TestOptions options = TestOptions.None, [CallerMemberName] string testCase = "") + // NOTE: Only use this method from a [Fact]. [Theory] should always use the below method to generate a unique testCase name per combination. + protected async Task InvokeGeneratorAndCompileFromFact([CallerMemberName] string testCase = "") + { + await this.InvokeGeneratorAndCompile(testCase); + } + + protected async Task InvokeGeneratorAndCompile(string testCase, TestOptions options = TestOptions.None) { this.compilation = this.compilation.AddReferences(this.additionalReferences.Select(x => MetadataReference.CreateFromFile(x))); @@ -123,6 +129,11 @@ protected async Task InvokeGenerator(string outputPath, string testCase, TestOpt args.AddRange(["--key-file", this.keyFile]); } + if (this.parseOptions.LanguageVersion is LanguageVersion version && version < LanguageVersion.CSharp13) + { + args.AddRange(["--language-version", LanguageVersionFacts.ToDisplayString(version)]); + } + // Act var loggerWriter = new TestOutputWriter(this.Logger); var program = new Program(loggerWriter, loggerWriter); @@ -145,7 +156,7 @@ protected async Task CompileGeneratedFilesWithSourceGenerators(string outputPath foreach (string filePath in generatedFiles) { string sourceCode = await File.ReadAllTextAsync(filePath); - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath, options: this.parseOptions); syntaxTrees.Add(syntaxTree); } @@ -155,7 +166,7 @@ protected async Task CompileGeneratedFilesWithSourceGenerators(string outputPath [assembly: DisableRuntimeMarshalling] "; - SyntaxTree disableRuntimeMarshallingSyntaxTree = CSharpSyntaxTree.ParseText(disableRuntimeMarshallingSource, path: "DisableRuntimeMarshalling.cs"); + SyntaxTree disableRuntimeMarshallingSyntaxTree = CSharpSyntaxTree.ParseText(disableRuntimeMarshallingSource, path: "DisableRuntimeMarshalling.cs", options: this.parseOptions); syntaxTrees.Add(disableRuntimeMarshallingSyntaxTree); this.compilation = this.compilation.AddSyntaxTrees(syntaxTrees); @@ -174,7 +185,7 @@ protected async Task CompileGeneratedFilesWithSourceGenerators(string outputPath } // Create a GeneratorDriver with the source generators - var driver = CSharpGeneratorDriver.Create(sourceGenerators.Where(x => x is IIncrementalGenerator).Select(x => (IIncrementalGenerator)x).ToArray()); + GeneratorDriver driver = CSharpGeneratorDriver.Create(sourceGenerators.Where(x => x is IIncrementalGenerator).Select(GeneratorExtensions.AsSourceGenerator), parseOptions: this.parseOptions); // Run the source generators var generatorDriver = driver.RunGeneratorsAndUpdateCompilation(this.compilation, out var newCompilation, out var generatorDiagnostics);