[release/8.0] Fix: Config binder generator doesn't generate code when named arguments...
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Tue, 19 Sep 2023 04:38:03 +0000 (21:38 -0700)
committerGitHub <noreply@github.com>
Tue, 19 Sep 2023 04:38:03 +0000 (21:38 -0700)
* Fix Named parameters bug

* Test the generator only, don't compare generated file row by row

* Add other named parameter combinatios for other overloads in the test, add test for OptionsBuilder... and ServiceCollection extensins

* Adjust line numbers with source generator updates

* Move similar code section into helper method, don't exact exact line count

* Apply feedbacks

---------

Co-authored-by: Buyaa Namnan <bunamnan@microsoft.com>
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/ConfigurationBinder.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBinderTests.Generator.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.Options.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs
src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/ConfigurationExtensionsTest.Generator.cs

index 9feafd7..3996142 100644 (file)
@@ -77,7 +77,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                     _ => throw new InvalidOperationException()
                 };
 
-                IArgumentOperation instanceArg = operation.Arguments[instanceIndex];
+                IArgumentOperation instanceArg = GetArgumentForParameterAtIndex(operation.Arguments, instanceIndex);
                 if (instanceArg.Parameter.Type.SpecialType != SpecialType.System_Object)
                 {
                     return;
@@ -119,6 +119,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                     };
             }
 
+            private static IArgumentOperation GetArgumentForParameterAtIndex(ImmutableArray<IArgumentOperation> arguments, int parameterIndex)
+            {
+                foreach (var argument in arguments)
+                {
+                    if (argument.Parameter?.Ordinal == parameterIndex)
+                    {
+                        return argument;
+                    }
+                }
+
+                throw new InvalidOperationException();
+            }
+
             private void ParseGetInvocation(BinderInvocation invocation)
             {
                 IInvocationOperation operation = invocation.Operation!;
@@ -158,7 +171,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 }
                 else
                 {
-                    ITypeOfOperation? typeOfOperation = operation.Arguments[1].ChildOperations.FirstOrDefault() as ITypeOfOperation;
+                    ITypeOfOperation? typeOfOperation = GetArgumentForParameterAtIndex(operation.Arguments, 1).ChildOperations.FirstOrDefault() as ITypeOfOperation;
                     type = typeOfOperation?.TypeOperand;
 
                     if (paramCount is 2)
@@ -218,7 +231,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                         return;
                     }
 
-                    ITypeOfOperation? typeOfOperation = operation.Arguments[1].ChildOperations.FirstOrDefault() as ITypeOfOperation;
+                    ITypeOfOperation? typeOfOperation = GetArgumentForParameterAtIndex(operation.Arguments, 1).ChildOperations.FirstOrDefault() as ITypeOfOperation;
                     type = typeOfOperation?.TypeOperand;
 
                     if (paramCount is 3)
index 1ffb263..5b6d824 100644 (file)
@@ -317,7 +317,7 @@ namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests
         /// <summary>
         /// These are regression tests for https://github.com/dotnet/runtime/issues/90909.
         /// Ensure that we don't emit root interceptors to handle types/members that
-        /// are inaccessible to the generated helpers. Tests for inaccessbile transitive members
+        /// are inaccessible to the generated helpers. Tests for inaccessible transitive members
         /// are covered in the shared (reflection/src-gen) <see cref="ConfigurationBinderTests"/>,
         /// e.g. <see cref="NonPublicModeGetStillIgnoresReadonly"/>.
         /// </summary>
index c3d1ce8..4480ab4 100644 (file)
@@ -60,6 +60,81 @@ namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests
         public async Task Configure_T_name_BinderOptions() =>
             await VerifyAgainstBaselineUsingFile("Configure_T_name_BinderOptions.generated.txt", GetConfigureSource(@""""", section, _ => { }"), extType: ExtensionClassType.ServiceCollection);
 
+        [Theory]
+        [InlineData("OptionsConfigurationServiceCollectionExtensions.Configure<MyClass>(config: section, services: services);")]
+        [InlineData("""OptionsConfigurationServiceCollectionExtensions.Configure<MyClass>(name: "", config: section, services: services);""")]
+        [InlineData("OptionsConfigurationServiceCollectionExtensions.Configure<MyClass>(configureBinder: _ => { }, config: section, services: services);")]
+        [InlineData("""OptionsConfigurationServiceCollectionExtensions.Configure<MyClass>(configureBinder: _ => { }, config: section, name: "", services: services);""")]
+        [InlineData("""OptionsConfigurationServiceCollectionExtensions.Configure<MyClass>(name: "", services: services, configureBinder: _ => { }, config: section);""")]
+        public async Task Configure_T_NamedParameters_OutOfOrder(string row)
+        {
+            string source = $$"""
+                    using System.Collections.Generic;
+                    using Microsoft.Extensions.Configuration;
+                    using Microsoft.Extensions.DependencyInjection;
+                    
+                    public class Program
+                    {
+                        public static void Main()
+                        {
+                            ConfigurationBuilder configurationBuilder = new();
+                            IConfiguration config = configurationBuilder.Build();
+                            IConfigurationSection section = config.GetSection("MySection");
+                            ServiceCollection services = new();
+
+                            {{row}}
+                        }
+                    
+                        public class MyClass
+                        {
+                            public string MyString { get; set; }
+                            public int MyInt { get; set; }
+                            public List<int> MyList { get; set; }
+                            public Dictionary<string, string> MyDictionary { get; set; }
+                        }
+                    }
+                    """;
+
+            await VerifyThatSourceIsGenerated(source);
+        }
+
+        [Theory]
+        [InlineData("OptionsBuilderConfigurationExtensions.Bind(config: config, optionsBuilder: optionsBuilder);")]
+        [InlineData("OptionsBuilderConfigurationExtensions.Bind(configureBinder: _ => { }, config: config, optionsBuilder: optionsBuilder);")]
+        [InlineData("OptionsBuilderConfigurationExtensions.Bind(config: config, configureBinder: _ => { }, optionsBuilder: optionsBuilder);")]
+        public async Task Bind_T_NamedParameters_OutOfOrder(string row)
+        {
+            string source = $$"""
+                    using System.Collections.Generic;
+                    using Microsoft.Extensions.Configuration;
+                    using Microsoft.Extensions.DependencyInjection;
+                    using Microsoft.Extensions.Options;
+                    
+                    public class Program
+                    {
+                        public static void Main()
+                        {
+                            ConfigurationBuilder configurationBuilder = new();
+                            IConfiguration config = configurationBuilder.Build();
+                            var services = new ServiceCollection();
+                            OptionsBuilder<MyClass> optionsBuilder = new(services, "");
+
+                            {{row}}
+                        }
+                    
+                        public class MyClass
+                        {
+                            public string MyString { get; set; }
+                            public int MyInt { get; set; }
+                            public List<int> MyList { get; set; }
+                            public Dictionary<string, string> MyDictionary { get; set; }
+                        }
+                    }
+                    """;
+
+            await VerifyThatSourceIsGenerated(source);
+        }
+
         private string GetBindSource(string? configureActions = null) => $$"""
             using System.Collections.Generic;
             using Microsoft.Extensions.Configuration;
index c48148c..3c46f5f 100644 (file)
@@ -15,6 +15,112 @@ namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests
         public async Task Bind() =>
             await VerifyAgainstBaselineUsingFile("Bind.generated.txt", BindCallSampleCode, extType: ExtensionClassType.ConfigurationBinder);
 
+        [Theory]
+        [InlineData("ConfigurationBinder.Bind(instance: configObj, configuration: config);")]
+        [InlineData("""ConfigurationBinder.Bind(key: "", instance: configObj, configuration: config);""")]
+        [InlineData("""ConfigurationBinder.Bind(instance: configObj, key: "", configuration: config);""")]
+        [InlineData("ConfigurationBinder.Bind(configureOptions: _ => { }, configuration: config, instance: configObj);")]
+        [InlineData("ConfigurationBinder.Bind(configuration: config, configureOptions: _ => { }, instance: configObj);")]
+        public async Task Bind_NamedParameters_OutOfOrder(string row)
+        {
+            string source = $$"""
+                        using System.Collections.Generic;
+                        using Microsoft.Extensions.Configuration;
+
+                        public class Program
+                        {
+                            public static void Main()
+                            {
+                                ConfigurationBuilder configurationBuilder = new();
+                                IConfigurationRoot config = configurationBuilder.Build();
+
+                                MyClass configObj = new();
+                                {{row}}
+                            }
+
+                            public class MyClass
+                            {
+                                public string MyString { get; set; }
+                                public int MyInt { get; set; }
+                                public List<int> MyList { get; set; }
+                                public Dictionary<string, string> MyDictionary { get; set; }
+                            }
+                        }
+                    """;
+
+            await VerifyThatSourceIsGenerated(source);
+        }
+
+        [Theory]
+        [InlineData("var obj = ConfigurationBinder.Get(type: typeof(MyClass), configuration: config);")]
+        [InlineData("var obj = ConfigurationBinder.Get<MyClass>(configureOptions: _ => { }, configuration: config);")]
+        [InlineData("var obj = ConfigurationBinder.Get(configureOptions: _ => { }, type: typeof(MyClass), configuration: config);")]
+        [InlineData("var obj =  ConfigurationBinder.Get(type: typeof(MyClass), configureOptions: _ => { }, configuration: config);")]
+        public async Task Get_TypeOf_NamedParametersOutOfOrder(string row)
+        {
+            string source = $$"""
+                        using System.Collections.Generic;
+                        using Microsoft.Extensions.Configuration;
+
+                        public class Program
+                        {
+                            public static void Main()
+                            {
+                                ConfigurationBuilder configurationBuilder = new();
+                                IConfigurationRoot config = configurationBuilder.Build();
+
+                                MyClass configObj = new();
+                                {{row}}
+                            }
+
+                            public class MyClass
+                            {
+                                public string MyString { get; set; }
+                                public int MyInt { get; set; }
+                                public List<int> MyList { get; set; }
+                                public Dictionary<string, string> MyDictionary { get; set; }
+                            }
+                        }
+                    """;
+
+            await VerifyThatSourceIsGenerated(source);
+        }
+
+        [Theory]
+        [InlineData("""var str = ConfigurationBinder.GetValue(key: "key", configuration: config, type: typeof(string));""")]
+        [InlineData("""var str = ConfigurationBinder.GetValue<string>(key: "key", configuration: config);""")]
+        [InlineData("""var str = ConfigurationBinder.GetValue<string>(key: "key", defaultValue: "default", configuration: config);""")]
+        [InlineData("""var str = ConfigurationBinder.GetValue<string>(configuration: config, key: "key", defaultValue: "default");""")]
+        [InlineData("""var str = ConfigurationBinder.GetValue(defaultValue: "default", key: "key", configuration: config, type: typeof(string));""")]
+        [InlineData("""var str = ConfigurationBinder.GetValue(defaultValue: "default", type: typeof(string), key: "key", configuration: config);""")]
+        public async Task GetValue_NamedParametersOutOfOrder(string row)
+        {
+            string source = $$"""
+                        using System.Collections.Generic;
+                        using Microsoft.Extensions.Configuration;
+
+                        public class Program
+                        {
+                            public static void Main()
+                            {
+                                ConfigurationBuilder configurationBuilder = new();
+                                IConfigurationRoot config = configurationBuilder.Build();
+                                {{row}}
+                            }
+
+                            public class MyClass
+                            {
+                                public string MyString { get; set; }
+                                public int MyInt { get; set; }
+                                public List<int> MyList { get; set; }
+                                public Dictionary<string, string> MyDictionary { get; set; }
+                            }
+                        }
+                    """;
+
+            await VerifyThatSourceIsGenerated(source);
+        }
+
         [Fact]
         public async Task Bind_Instance()
         {
index 0053cb4..bee4d14 100644 (file)
@@ -85,6 +85,14 @@ namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests
             ServiceCollection,
         }
 
+        private static async Task VerifyThatSourceIsGenerated(string testSourceCode)
+        {
+            var (d, r) = await RunGenerator(testSourceCode);
+            Assert.Equal(1, r.Length);
+            Assert.Empty(d);
+            Assert.True(r[0].SourceText.Lines.Count > 10);
+        }
+
         private static async Task VerifyAgainstBaselineUsingFile(
             string filename,
             string testSourceCode,
index a18efc6..9b3aff8 100644 (file)
@@ -156,5 +156,22 @@ namespace Microsoft.Extensions.Options.ConfigurationExtensions.Tests
                 )
                 ;
         }
+
+        [Fact]
+        public void TestBindAndConfigureWithNamedParameters()
+        {
+            OptionsBuilder<FakeOptions>? optionsBuilder = CreateOptionsBuilder();
+            IServiceCollection services = new ServiceCollection();
+
+            OptionsBuilderConfigurationExtensions.Bind(config: s_emptyConfig, optionsBuilder: optionsBuilder);
+            OptionsBuilderConfigurationExtensions.Bind(configureBinder: _ => { }, config: s_emptyConfig, optionsBuilder: optionsBuilder);
+
+            OptionsBuilderConfigurationExtensions.BindConfiguration(configureBinder: _ => { }, configSectionPath: "path", optionsBuilder: optionsBuilder);
+
+            OptionsConfigurationServiceCollectionExtensions.Configure<FakeOptions>(config: s_emptyConfig, services: services);
+            OptionsConfigurationServiceCollectionExtensions.Configure<FakeOptions>(name: "", config: s_emptyConfig, services: services);
+            OptionsConfigurationServiceCollectionExtensions.Configure<FakeOptions>(configureBinder: _ => { }, config: s_emptyConfig, services: services);
+            OptionsConfigurationServiceCollectionExtensions.Configure<FakeOptions>(name: "", configureBinder: _ => { }, config: s_emptyConfig, services: services);
+        }
     }
 }