diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsConfigurationServiceCollectionExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsConfigurationServiceCollectionExtensions.cs index 0348eb5047e970..41eeb8b657342d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsConfigurationServiceCollectionExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter/OptionsConfigurationServiceCollectionExtensions.cs @@ -62,7 +62,7 @@ private void EmitConfigureMethods() _writer.WriteLine($$""" OptionsServiceCollectionExtensions.AddOptions({{Identifier.services}}); {{Identifier.services}}.{{Identifier.AddSingleton}}<{{Identifier.IOptionsChangeTokenSource}}<{{Identifier.TOptions}}>>(new {{Identifier.ConfigurationChangeTokenSource}}<{{Identifier.TOptions}}>({{Identifier.name}}, {{Identifier.configuration}})); - return {{Identifier.services}}.{{Identifier.AddSingleton}}<{{optionsNamespaceName}}.IConfigureOptions<{{Identifier.TOptions}}>>(new {{optionsNamespaceName}}.ConfigureNamedOptions<{{Identifier.TOptions}}>({{Identifier.name}}, {{Identifier.obj}} => {{nameof(MethodsToGen_CoreBindingHelper.BindCoreMain)}}({{Identifier.configuration}}, {{Identifier.obj}}, typeof({{Identifier.TOptions}}){{Identifier.configureOptions}}))); + return {{Identifier.services}}.{{Identifier.AddSingleton}}<{{optionsNamespaceName}}.IConfigureOptions<{{Identifier.TOptions}}>>(new {{optionsNamespaceName}}.ConfigureNamedOptions<{{Identifier.TOptions}}>({{Identifier.name}}, {{Identifier.obj}} => {{nameof(MethodsToGen_CoreBindingHelper.BindCoreMain)}}({{Identifier.configuration}}, {{Identifier.obj}}, typeof({{Identifier.TOptions}}), {{Identifier.configureOptions}}))); """); EmitEndBlock(); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/InterceptorLocationInfo.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/InterceptorLocationInfo.cs index d1dc4f4afa7e7e..441acbe6a7444f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/InterceptorLocationInfo.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/InterceptorLocationInfo.cs @@ -15,28 +15,27 @@ internal sealed record InterceptorLocationInfo { public InterceptorLocationInfo(IInvocationOperation operation) { - SyntaxNode operationSyntax = operation.Syntax; - TextSpan operationSpan = operationSyntax.Span; - SyntaxTree operationSyntaxTree = operationSyntax.SyntaxTree; - - FilePath = GetInterceptorFilePath(operationSyntaxTree, operation.SemanticModel?.Compilation.Options.SourceReferenceResolver); - - FileLinePositionSpan span = operationSyntaxTree.GetLineSpan(operationSpan); - LineNumber = span.StartLinePosition.Line + 1; - - // Calculate the character offset to the end of the binding invocation detected. - int invocationLength = ((MemberAccessExpressionSyntax)((InvocationExpressionSyntax)operationSyntax).Expression).Expression.Span.Length; - CharacterNumber = span.StartLinePosition.Character + invocationLength + 2; + MemberAccessExpressionSyntax memberAccessExprSyntax = ((MemberAccessExpressionSyntax)((InvocationExpressionSyntax)operation.Syntax).Expression); + SyntaxTree operationSyntaxTree = operation.Syntax.SyntaxTree; + TextSpan memberNameSpan = memberAccessExprSyntax.Name.Span; + FileLinePositionSpan linePosSpan = operationSyntaxTree.GetLineSpan(memberNameSpan); + + LineNumber = linePosSpan.StartLinePosition.Line + 1; + CharacterNumber = linePosSpan.StartLinePosition.Character + 1; + FilePath = GetInterceptorFilePath(); + + // Use the same logic used by the interceptors API for resolving the source mapped value of a path. + // https://github.com/dotnet/roslyn/blob/f290437fcc75dad50a38c09e0977cce13a64f5ba/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs#L1063-L1064 + string GetInterceptorFilePath() + { + SourceReferenceResolver? sourceReferenceResolver = operation.SemanticModel?.Compilation.Options.SourceReferenceResolver; + return sourceReferenceResolver?.NormalizePath(operationSyntaxTree.FilePath, baseFilePath: null) ?? operationSyntaxTree.FilePath; + } } public string FilePath { get; } public int LineNumber { get; } public int CharacterNumber { get; } - - // Utilize the same logic used by the interceptors API for resolving the source mapped value of a path. - // https://github.com/dotnet/roslyn/blob/f290437fcc75dad50a38c09e0977cce13a64f5ba/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs#L1063-L1064 - private static string GetInterceptorFilePath(SyntaxTree tree, SourceReferenceResolver? resolver) => - resolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; } internal sealed record ConfigurationBinderInterceptorInfo @@ -52,7 +51,7 @@ public void RegisterOverloadInfo(MethodsToGen_ConfigurationBinder overload, Type } public OverloadInterceptorInfo GetOverloadInfo(MethodsToGen_ConfigurationBinder overload) => - DetermineOverload(overload, initIfNull: false) ?? throw new ArgumentNullException(nameof(overload)); + DetermineOverload(overload, initIfNull: false) ?? throw new ArgumentOutOfRangeException(nameof(overload)); private OverloadInterceptorInfo? DetermineOverload(MethodsToGen_ConfigurationBinder overload, bool initIfNull) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs index a1d1a72ffab20f..d6521ed86dfdec 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs @@ -160,5 +160,17 @@ public class CustomICollectionWithoutAnAddMethod : ICollection public int Count => _items.Count; public bool IsReadOnly => false; } + + public interface IGeolocation + { + public double Latitude { get; set; } + public double Longitude { get; set; } + } + + public sealed record GeolocationRecord : IGeolocation + { + public double Latitude { get; set; } + public double Longitude { get; set; } + } #endregion } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs index 1e537b407963d9..76ed9d959e6220 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -667,12 +667,6 @@ public struct StructWithParameterlessAndParameterizedCtor public int MyInt { get; } } - public interface IGeolocation - { - public double Latitude { get; set; } - public double Longitude { get; set; } - } - [TypeConverter(typeof(GeolocationTypeConverter))] public struct Geolocation : IGeolocation { @@ -704,12 +698,6 @@ public sealed class GeolocationClass : IGeolocation public double Longitude { get; set; } } - public sealed record GeolocationRecord : IGeolocation - { - public double Latitude { get; set; } - public double Longitude { get; set; } - } - public class GeolocationWrapper { public Geolocation Location { get; set; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T.generated.txt index 633196e7a742d5..ff590d8398f3a1 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T.generated.txt @@ -68,7 +68,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration OptionsServiceCollectionExtensions.AddOptions(services); services.AddSingleton>(new ConfigurationChangeTokenSource(name, configuration)); - return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions)configureOptions))); + return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions), configureOptions))); } #endregion IServiceCollection extensions. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T_BinderOptions.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T_BinderOptions.generated.txt index fb5b4b4ad721d8..b7b6ef86a4c056 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T_BinderOptions.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/OptionsBuilder/Bind_T_BinderOptions.generated.txt @@ -62,7 +62,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration OptionsServiceCollectionExtensions.AddOptions(services); services.AddSingleton>(new ConfigurationChangeTokenSource(name, configuration)); - return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions)configureOptions))); + return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions), configureOptions))); } #endregion IServiceCollection extensions. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T.generated.txt index 7f626f0e27c527..7a956f99b57b13 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T.generated.txt @@ -53,7 +53,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration OptionsServiceCollectionExtensions.AddOptions(services); services.AddSingleton>(new ConfigurationChangeTokenSource(name, configuration)); - return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions)configureOptions))); + return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions), configureOptions))); } #endregion IServiceCollection extensions. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_BinderOptions.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_BinderOptions.generated.txt index 5848c2412f9b78..399f6ffe35b935 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_BinderOptions.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_BinderOptions.generated.txt @@ -53,7 +53,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration OptionsServiceCollectionExtensions.AddOptions(services); services.AddSingleton>(new ConfigurationChangeTokenSource(name, configuration)); - return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions)configureOptions))); + return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions), configureOptions))); } #endregion IServiceCollection extensions. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name.generated.txt index 91226d730166f1..197e268561f7eb 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name.generated.txt @@ -53,7 +53,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration OptionsServiceCollectionExtensions.AddOptions(services); services.AddSingleton>(new ConfigurationChangeTokenSource(name, configuration)); - return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions)configureOptions))); + return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions), configureOptions))); } #endregion IServiceCollection extensions. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name_BinderOptions.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name_BinderOptions.generated.txt index 8c9ccaa71a779f..0cb5881950dee3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name_BinderOptions.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/ServiceCollection/Configure_T_name_BinderOptions.generated.txt @@ -47,7 +47,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration OptionsServiceCollectionExtensions.AddOptions(services); services.AddSingleton>(new ConfigurationChangeTokenSource(name, configuration)); - return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions)configureOptions))); + return services.AddSingleton>(new Microsoft.Extensions.Options.ConfigureNamedOptions(name, obj => BindCoreMain(configuration, obj, typeof(TOptions), configureOptions))); } #endregion IServiceCollection extensions. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBinderTests.Generator.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBinderTests.Generator.cs new file mode 100644 index 00000000000000..0e4a97b85b548a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBinderTests.Generator.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests +{ + public partial class ConfigurationBinderTests : ConfigurationBinderTestsBase + { + // These are regression tests for https://github.com/dotnet/runtime/issues/90851 + // Source Generator Interceptors rely on identifying an accurate invocation + // source location (line and character positions). These tests cover newline + // and whitespace scenarios to ensure the interceptors get wired up correctly. + + [Fact] + public void TestBindingInvocationsWithNewlines_GetMethodTypeArg() + { + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{""Longitude"":1,""Latitude"":2}"); + + // Newline between the configuration instance and the binding invocation (with the dot on the first line) + GeolocationRecord record1 = (GeolocationRecord)configuration. + Get(typeof(GeolocationRecord), _ => { }); + + AssertRecordIsBound(record1, 1, 2); + + // Newline between the configuration instance and the binding invocation (with the dot on the second line) + GeolocationRecord record2 = (GeolocationRecord)configuration + .Get(typeof(GeolocationRecord), _ => { }); + + AssertRecordIsBound(record2, 1, 2); + + // Newlines between the instance, the invocation, and the arguments + GeolocationRecord record3 = (GeolocationRecord)configuration + .Get( + typeof(GeolocationRecord), + _ => { } + ); + + AssertRecordIsBound(record3, 1, 2); + + // Newlines before and after the instance (with the dot on the first line) + GeolocationRecord record4 = (GeolocationRecord) + configuration. + Get(typeof(GeolocationRecord), _ => { }); + + AssertRecordIsBound(record4, 1, 2); + + // Newlines before and after the instance (with the dot on the second line) + GeolocationRecord record5 = (GeolocationRecord) + configuration + .Get(typeof(GeolocationRecord), _ => { }); + + AssertRecordIsBound(record5, 1, 2); + + // Newlines in every place possible + GeolocationRecord + record6 + = + ( + GeolocationRecord + ) + configuration + . + Get + ( + typeof + ( + GeolocationRecord + ) + , + _ + => + { + } + ) + ; + + AssertRecordIsBound(record6, 1, 2); + } + + [Fact] + public void TestBindingInvocationsWithNewlines_GetMethodGeneric() + { + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{""Longitude"":1,""Latitude"":2}"); + + // Newline between the invocation method name and the generic type argument + GeolocationRecord record1 = configuration.Get + (); + + AssertRecordIsBound(record1, 1, 2); + + // Newlines on either side of the generic type argument + GeolocationRecord record2 = configuration.Get< + GeolocationRecord + >(); + + AssertRecordIsBound(record2, 1, 2); + + // Newlines in every place possible + GeolocationRecord + record3 + = + configuration + . + Get + < + GeolocationRecord + > + () + ; + + AssertRecordIsBound(record3, 1, 2); + } + + [Fact] + public void TestBindingInvocationsWithNewlines_BindExtensionMethod() + { + // Newline between the configuration instance and the extension method invocation + GeolocationRecord record1 = new GeolocationRecord(); + TestHelpers.GetConfigurationFromJsonString(@"{""Longitude"":1,""Latitude"":2}") + .Bind(record1); + + AssertRecordIsBound(record1, 1, 2); + + // Newlines between the method that returns the instance and the extension method invocation + GeolocationRecord record2 = new GeolocationRecord(); + TestHelpers + .GetConfigurationFromJsonString(@"{""Longitude"":1,""Latitude"":2}") + .Bind(record2); + + AssertRecordIsBound(record2, 1, 2); + + // Newlines within the argument to the method returning the configuration and around the extension method argument + GeolocationRecord record3 = new GeolocationRecord(); + TestHelpers + .GetConfigurationFromJsonString(@"{""Longitude"":1, + ""Latitude"":2} + ") + .Bind( + record3 + ); + + AssertRecordIsBound(record3, 1, 2); + + // Newlines in every place possible + GeolocationRecord record4 = new GeolocationRecord(); + TestHelpers + . + GetConfigurationFromJsonString + ( + @"{""Longitude"":1, ""Latitude"":2}" + ) + . + Bind + ( + record4 + ) + ; + + AssertRecordIsBound(record4, 1, 2); + } + + [Fact] + public void TestBindingInvocationsWithNewlines_BindStaticMethod() + { + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{""Longitude"":1,""Latitude"":2}"); + + // Newline between the class and the static method invocation (with the dot on the first line) + GeolocationRecord record1 = new GeolocationRecord(); + ConfigurationBinder. + Bind(configuration, record1); + + // Newline between the class and the static method invocation (with the dot on the second line) + GeolocationRecord record2 = new GeolocationRecord(); + ConfigurationBinder + .Bind(configuration, record2); + + AssertRecordIsBound(record2, 1, 2); + + // Newline before the arguments + GeolocationRecord record3 = new GeolocationRecord(); + ConfigurationBinder.Bind( + configuration, record3); + + AssertRecordIsBound(record3, 1, 2); + + // Newlines in every place possible + GeolocationRecord record4 = new GeolocationRecord(); + ConfigurationBinder + . + Bind + ( + configuration + , + record4 + ) + ; + + AssertRecordIsBound(record4, 1, 2); + } + + [Fact] + public void TestBindingInvocationsWithNewlines_GetValueMethod() + { + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(@"{""Longitude"":1,""Latitude"":2}"); + + // Newline between the configuration instance and the binding invocation (with the dot on the first line) + int lat1 = configuration. + GetValue("Latitude"); + + Assert.Equal(2, lat1); + + // Newline between the configuration instance and the binding invocation (with the dot on the second line) + int lat2 = configuration + .GetValue("Latitude"); + + Assert.Equal(2, lat2); + + // Newlines in every place possible + long + lat3 + = + configuration + . + GetValue + < + int + > + ( + "Latitude" + ) + ; + Assert.Equal(2, lat3); + + // Newlines and pragmas wrapped around the generic type argument + long lat4 = configuration.GetValue< +#if DEBUG + int +#else + long +#endif + >("Latitude"); + + Assert.Equal(2, lat4); + } + + private static void AssertRecordIsBound(GeolocationRecord record, int longitude, int latitude) + { + Assert.Equal((longitude, latitude), (record.Longitude, record.Latitude)); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.Baselines.Options.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.Options.cs similarity index 98% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.Baselines.Options.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.Options.cs index c9eb9c70927f99..0126744eacbfa4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.Baselines.Options.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.Options.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests +namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests { public partial class ConfigurationBindingGeneratorTests { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.Baselines.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs similarity index 99% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.Baselines.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs index b5c3fb49c5e7eb..65e812d30b420f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.Baselines.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs @@ -8,7 +8,7 @@ using Microsoft.CodeAnalysis.CSharp; using Xunit; -namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests +namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests { public partial class ConfigurationBindingGeneratorTests { @@ -649,7 +649,6 @@ public interface ICustomSet : ISet await VerifyAgainstBaselineUsingFile("Collections.generated.txt", source, assessDiagnostics: (d) => { - Console.WriteLine((d.Where(diag => diag.Id == Diagnostics.TypeNotSupported.Id).Count(), d.Where(diag => diag.Id == Diagnostics.PropertyNotSupported.Id).Count())); Assert.Equal(3, d.Where(diag => diag.Id == Diagnostics.TypeNotSupported.Id).Count()); Assert.Equal(6, d.Where(diag => diag.Id == Diagnostics.PropertyNotSupported.Id).Count()); }); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs similarity index 94% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs index a512c5efe495b1..68f8a66612e67f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigurationBindingGeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -14,13 +14,14 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Binder.SourceGeneration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests; using SourceGenerators.Tests; using Xunit; -namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests +namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests { [ActiveIssue("https://github.com/dotnet/runtime/issues/52062", TestPlatforms.Browser)] public partial class ConfigurationBindingGeneratorTests : ConfigurationBinderTestsBase @@ -388,11 +389,24 @@ private static async Task VerifyAgainstBaselineUsingFile( var (d, r) = await RunGenerator(testSourceCode, languageVersion); bool success = RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText, out string errorMessage); -#if !SKIP_BASELINES +#if UPDATE_BASELINES + if (!success) + { + string? repoRootDir = Environment.GetEnvironmentVariable("RepoRootDir"); + Assert.True(repoRootDir is not null, "To update baselines, specifiy the root runtime repo dir"); + + IEnumerable lines = r[0].SourceText.Lines.Select(l => l.ToString()); + string source = string.Join(Environment.NewLine, lines).TrimEnd(Environment.NewLine.ToCharArray()) + Environment.NewLine; + path = Path.Combine($"{repoRootDir}\\src\\libraries\\Microsoft.Extensions.Configuration.Binder\\tests\\SourceGenerationTests\\", path); + + await File.WriteAllTextAsync(path, source).ConfigureAwait(false); + success = true; + } +#endif + Assert.Single(r); (assessDiagnostics ?? ((d) => Assert.Empty(d))).Invoke(d); Assert.True(success, errorMessage); -#endif } private static async Task<(ImmutableArray, ImmutableArray)> RunGenerator( diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj index cfd45c365d42a0..94d95564a47352 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj @@ -11,7 +11,7 @@ $(DefineConstants);BUILDING_SOURCE_GENERATOR_TESTS;ROSLYN4_0_OR_GREATER;ROSLYN4_4_OR_GREATER $(DefineConstants);LAUNCH_DEBUGGER - $(DefineConstants);SKIP_BASELINES + $(DefineConstants);UPDATE_BASELINES @@ -28,6 +28,7 @@ + @@ -50,9 +51,9 @@ PreserveNewest - - - + + + diff --git a/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/ConfigurationExtensionsTests.cs b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/ConfigurationExtensionsTests.cs new file mode 100644 index 00000000000000..95cee559072f07 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/ConfigurationExtensionsTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Options.ConfigurationExtensions.Tests +{ + public partial class ConfigurationExtensionsTests + { + private static IConfiguration s_emptyConfig { get; } = new ConfigurationBuilder().Build(); + + private static OptionsBuilder CreateOptionsBuilder() + { + var services = new ServiceCollection(); + return new OptionsBuilder(services, Options.DefaultName); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/OptionsBuilderConfigurationExtensionsTests.cs b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/OptionsBuilderConfigurationExtensionsTests.cs index 6d1d4a018f097e..d90122ef74d8bc 100644 --- a/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/OptionsBuilderConfigurationExtensionsTests.cs +++ b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/Common/OptionsBuilderConfigurationExtensionsTests.cs @@ -31,7 +31,8 @@ public static void BindConfiguration_ThrowsForNullConfigurationSectionPath() Assert.Throws("configSectionPath", () => { - optionsBuilder.BindConfiguration(configSectionPath); + optionsBuilder + .BindConfiguration(configSectionPath); }); } @@ -170,8 +171,8 @@ public static void BindConfiguration_UpdatesOptionOnConfigurationUpdate() services.AddSingleton(new ConfigurationBuilder() .Add(configSource) .Build()); - OptionsBuilder optionsBuilder = services.AddOptions(); - _ = optionsBuilder.BindConfiguration(configSectionName); + _ = services.AddOptions() + .BindConfiguration(configSectionName); using ServiceProvider serviceProvider = services.BuildServiceProvider(); var optionsMonitor = serviceProvider.GetRequiredService>(); bool updateHasRun = false; diff --git a/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/ConfigurationExtensionsTest.Generator.cs b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/ConfigurationExtensionsTest.Generator.cs new file mode 100644 index 00000000000000..a18efc6909b71d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/ConfigurationExtensionsTest.Generator.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Options.ConfigurationExtensions.Tests +{ + public partial class ConfigurationExtensionsTests + { + // These are regression tests for https://github.com/dotnet/runtime/issues/90851 + // Source Generator Interceptors rely on identifying an accurate invocation + // source location (line and character positions). These tests cover newline + // and whitespace scenarios to ensure the interceptors get wired up correctly. + + [Fact] + public void TestBindingInvocationsWithNewlines_BindExtension() + { + OptionsBuilder? optionsBuilder = CreateOptionsBuilder(); + + // Newline between instance and invocation using configureBinder argument (with the dot on the first line) + optionsBuilder. + Bind(s_emptyConfig, configureBinder: null); + + // Newline between instance and invocation using configureBinder argument (with the dot on the second line) + optionsBuilder + .Bind(s_emptyConfig, configureBinder: null); + + // Newline between instance and invocation (with the dot on the first line) + optionsBuilder. + Bind(s_emptyConfig); + + // Newline between instance and invocation (with the dot on the second line) + optionsBuilder + .Bind(s_emptyConfig); + + // Newlines in every place possible + optionsBuilder + . + Bind + ( + s_emptyConfig + , + configureBinder + : + null + ) + ; + } + + [Fact] + public void TestBindingInvocationsWithNewlines_BindConfigurationExtension() + { + OptionsBuilder? optionsBuilder = CreateOptionsBuilder(); + + // Newline between instance and invocation using configureBinder argument (with the dot on the first line) + optionsBuilder. + BindConfiguration(configSectionPath: "path", + _ => { }); + + // Newline between instance and invocation using configureBinder argument (with the dot on the second line) + optionsBuilder + .BindConfiguration(configSectionPath: "path", + _ => { }); + + // Newlines between the instance and invocation and within the arguments. No indentation before invocation. + optionsBuilder. + BindConfiguration( + configSectionPath: "path", + _ => { }); + + // Newlines in every place possible + optionsBuilder + . + BindConfiguration + ( + configSectionPath + : + "path" + , + _ + => + { + } + ) + ; + } + + [Fact] + public void TestBindingInvocationsWithNewlines_ConfigureExtension() + { + OptionsBuilder? optionsBuilder = CreateOptionsBuilder(); + IServiceCollection services = new ServiceCollection(); + + // Newlines between each method call + services + .Configure(s_emptyConfig) + .AddOptions(); + + // Newlines in every place possible + services + . + Configure + < + FakeOptions + > + ( + name + : + null! + , + s_emptyConfig + ) + ; + } + + [Fact] + public void TestBindingInvocationsWithNewlines_StaticCalls() + { + OptionsBuilder? optionsBuilder = CreateOptionsBuilder(); + IServiceCollection services = new ServiceCollection(); + + // Bind: Newlines in every place possible + OptionsBuilderConfigurationExtensions + . + Bind + ( + optionsBuilder + , + s_emptyConfig + ) + ; + + // // BindConfiguration: Newlines in every place possible + OptionsBuilderConfigurationExtensions + . + BindConfiguration + ( + optionsBuilder + , + "path" + ); + + // Configure: Newlines in every place possible + OptionsConfigurationServiceCollectionExtensions + . + Configure + < + FakeOptions + > + ( + services + , + s_emptyConfig + ) + ; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/Microsoft.Extensions.Options.ConfigurationExtensions.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/Microsoft.Extensions.Options.ConfigurationExtensions.SourceGeneration.Tests.csproj index 0676fddc289826..f08d2bd649bc3f 100644 --- a/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/Microsoft.Extensions.Options.ConfigurationExtensions.SourceGeneration.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Options.ConfigurationExtensions/tests/SourceGenerationTests/Microsoft.Extensions.Options.ConfigurationExtensions.SourceGeneration.Tests.csproj @@ -20,10 +20,12 @@ + - + +