Skip to content

Commit 8b8b15c

Browse files
authored
chore: add .NET Framework polyfills and fix MAUI Visibility ambiguity (#4)
* Fix .NET Framework build errors and MAUI Visibility ambiguity - Add RequiresUnreferencedCodeAttribute polyfill for net462/net472/net481 targets where the attribute is inaccessible (only public in .NET 5+) - Add ModuleInitializerAttribute polyfill for benchmark net462 target - Fix CS0104 Visibility ambiguity in MAUI project on Windows targets by using type aliases instead of namespace imports * Fix remaining .NET Framework polyfill and visibility issues - Add InternalsVisibleTo from ReactiveUI.Binding to Wpf, WinForms, and Maui projects so they can access internal polyfill attributes (RequiresUnreferencedCodeAttribute, NotNullWhenAttribute, etc.) - Add missing using for POCOObservableForProperty in MAUI's DependencyObjectObservableForProperty.cs - Emit ModuleInitializerAttribute polyfill from source generator for consumer projects targeting .NET Framework (same pattern as existing CallerArgumentExpressionAttribute polyfill) * Remove all polyfill emission from source generator The generator should never emit polyfills into consumer projects. Instead it detects platform capabilities and generates appropriate code: - Remove CallerArgumentExpressionAttribute polyfill emission; the supportsCallerArgExpr flag now checks both C# 10+ language version AND that the attribute exists in the compilation, falling back to file/line dispatch when unavailable - Remove [ModuleInitializer] from generated registration code entirely - Replace [ModuleInitializer] in ReactiveUI benchmarks with explicit static constructor calls using an atomic bool guard pattern - Update snapshot tests
1 parent bb1b17d commit 8b8b15c

71 files changed

Lines changed: 85 additions & 114 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ReactiveUI.Binding.Maui/BooleanToVisibilityTypeConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// See the LICENSE file in the project root for full license information.
44

55
#if WINUI_TARGET
6-
using Microsoft.UI.Xaml;
6+
using Visibility = Microsoft.UI.Xaml.Visibility;
77
#else
8-
using Microsoft.Maui;
8+
using Visibility = Microsoft.Maui.Visibility;
99
#endif
1010

1111
namespace ReactiveUI.Binding.Maui;

src/ReactiveUI.Binding.Maui/DependencyObjectObservableForProperty.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
using Microsoft.UI.Xaml;
1111

12+
using ReactiveUI.Binding.ObservableForProperty;
13+
1214
namespace ReactiveUI.Binding.Maui;
1315

1416
/// <summary>

src/ReactiveUI.Binding.Maui/VisibilityToBooleanTypeConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// See the LICENSE file in the project root for full license information.
44

55
#if WINUI_TARGET
6-
using Microsoft.UI.Xaml;
6+
using Visibility = Microsoft.UI.Xaml.Visibility;
77
#else
8-
using Microsoft.Maui;
8+
using Visibility = Microsoft.Maui.Visibility;
99
#endif
1010

1111
namespace ReactiveUI.Binding.Maui;

src/ReactiveUI.Binding.SourceGenerators/BindingGenerator.cs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,15 @@ public class BindingGenerator : IIncrementalGenerator
2222
/// <inheritdoc/>
2323
public void Initialize(IncrementalGeneratorInitializationContext context)
2424
{
25-
// Detect C# language version for CallerArgumentExpression support (C# 10+)
26-
var supportsCallerArgExpr = context.ParseOptionsProvider.Select(static (opts, _) =>
27-
opts is CSharpParseOptions csharpOpts
28-
&& csharpOpts.LanguageVersion >= LanguageVersion.CSharp10);
29-
30-
// Conditionally emit CallerArgumentExpression polyfill when C# 10+ but attribute is missing
31-
var needsPolyfill = supportsCallerArgExpr
25+
// Detect whether the consumer project supports CallerArgumentExpression (C# 10+ AND attribute available).
26+
// When supported, invocation generators use expression-text dispatch; otherwise file/line dispatch.
27+
var supportsCallerArgExpr = context.ParseOptionsProvider
3228
.Combine(context.CompilationProvider)
3329
.Select(static (data, _) =>
34-
data.Left && data.Right.GetTypeByMetadataName(
35-
Constants.CallerArgumentExpressionAttributeMetadataName) is null);
36-
37-
context.RegisterSourceOutput(needsPolyfill, static (ctx, needs) =>
38-
{
39-
if (needs)
40-
{
41-
ctx.AddSource(
42-
"CallerArgumentExpressionAttribute.g.cs",
43-
Constants.CallerArgumentExpressionAttributeSource);
44-
}
45-
});
30+
data.Left is CSharpParseOptions csharpOpts
31+
&& csharpOpts.LanguageVersion >= LanguageVersion.CSharp10
32+
&& data.Right.GetTypeByMetadataName(
33+
Constants.CallerArgumentExpressionAttributeMetadataName) is not null);
4634

4735
// Pipeline A: Shared type detection
4836
// One pass: sets flags for IRO, INPC, WpfDP, WinUIDP, KVO, etc.

src/ReactiveUI.Binding.SourceGenerators/Constants.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,4 @@ internal static class Constants
4545
internal const string CallerFilePathAttributeName = "System.Runtime.CompilerServices.CallerFilePathAttribute";
4646
internal const string CallerLineNumberAttributeName = "System.Runtime.CompilerServices.CallerLineNumberAttribute";
4747
internal const string CallerArgumentExpressionAttributeMetadataName = "System.Runtime.CompilerServices.CallerArgumentExpressionAttribute";
48-
49-
/// <summary>
50-
/// Polyfill source for CallerArgumentExpressionAttribute when targeting runtimes that don't include it.
51-
/// Only emitted when C# 10+ is detected and the attribute is not already available.
52-
/// </summary>
53-
internal const string CallerArgumentExpressionAttributeSource = """
54-
// <auto-generated/>
55-
#pragma warning disable
56-
namespace System.Runtime.CompilerServices
57-
{
58-
[global::System.AttributeUsage(global::System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
59-
internal sealed class CallerArgumentExpressionAttribute : global::System.Attribute
60-
{
61-
public CallerArgumentExpressionAttribute(string parameterName)
62-
{
63-
ParameterName = parameterName;
64-
}
65-
66-
public string ParameterName { get; }
67-
}
68-
}
69-
""";
7048
}

src/ReactiveUI.Binding.SourceGenerators/Generators/RegistrationGenerator.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace ReactiveUI.Binding.SourceGenerators.Generators;
1313

1414
/// <summary>
1515
/// Consolidates all per-kind fallback generator results into a single RegisterSourceOutput.
16-
/// Generates all ICreatesObservableForProperty implementations and one [ModuleInitializer] class.
16+
/// Generates all ICreatesObservableForProperty implementations and a registration class.
1717
/// </summary>
1818
internal static class RegistrationGenerator
1919
{
@@ -58,7 +58,7 @@ internal static IncrementalValueProvider<ImmutableArray<ObservableTypeInfo>> Con
5858
}
5959

6060
/// <summary>
61-
/// Generates the consolidated registration output: all per-kind binder classes + [ModuleInitializer].
61+
/// Generates the consolidated registration output: all per-kind binder classes.
6262
/// </summary>
6363
/// <param name="context">The source production context.</param>
6464
/// <param name="allTypes">All detected observable type infos across all notification kinds.</param>
@@ -93,7 +93,6 @@ internal static class __GeneratedBinderRegistration
9393
/// <summary>
9494
/// Registers all generated binders with the Splat service locator.
9595
/// </summary>
96-
[global::System.Runtime.CompilerServices.ModuleInitializer]
9796
internal static void Initialize()
9897
{
9998
// Generated binder registrations will be added here in future phases.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
// Polyfill implementation adapted from SimonCropp/Polyfill
6+
// https://github.com/SimonCropp/Polyfill
7+
#if !NET
8+
9+
namespace System.Diagnostics.CodeAnalysis;
10+
11+
/// <summary>
12+
/// Indicates that the specified method requires dynamic access to code that is not referenced
13+
/// statically, for example through <see cref="System.Reflection"/>.
14+
/// </summary>
15+
/// <remarks>
16+
/// Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.requiresunreferencedcodeattribute.
17+
/// </remarks>
18+
[ExcludeFromCodeCoverage]
19+
[DebuggerNonUserCode]
20+
[AttributeUsage(
21+
AttributeTargets.Method |
22+
AttributeTargets.Constructor |
23+
AttributeTargets.Class,
24+
Inherited = false)]
25+
internal sealed class RequiresUnreferencedCodeAttribute : Attribute
26+
{
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="RequiresUnreferencedCodeAttribute"/> class
29+
/// with the specified message.
30+
/// </summary>
31+
/// <param name="message">A message that contains information about the usage of unreferenced code.</param>
32+
public RequiresUnreferencedCodeAttribute(string message) =>
33+
Message = message;
34+
35+
/// <summary>
36+
/// Gets a message that contains information about the usage of unreferenced code.
37+
/// </summary>
38+
public string Message { get; }
39+
40+
/// <summary>
41+
/// Gets or sets an optional URL that contains more information about the method,
42+
/// why it requires unreferenced code, and what options a consumer has to deal with it.
43+
/// </summary>
44+
public string? Url { get; set; }
45+
}
46+
47+
#else
48+
using System.Runtime.CompilerServices;
49+
50+
[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute))]
51+
#endif

src/ReactiveUI.Binding/ReactiveUI.Binding.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<ItemGroup>
1111
<InternalsVisibleTo Include="ReactiveUI.Binding.Tests" />
1212
<InternalsVisibleTo Include="ReactiveUI.Binding.GeneratedCode.Tests" />
13+
<InternalsVisibleTo Include="ReactiveUI.Binding.Wpf" />
14+
<InternalsVisibleTo Include="ReactiveUI.Binding.WinForms" />
15+
<InternalsVisibleTo Include="ReactiveUI.Binding.Maui" />
1316
</ItemGroup>
1417

1518
<ItemGroup>

src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ModuleInitializer.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,31 @@
22
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for full license information.
44

5-
using System.Runtime.CompilerServices;
6-
75
using ReactiveUI.Builder;
86

97
using Splat;
108

119
namespace ReactiveUI.Binding.Benchmarks;
1210

1311
/// <summary>
14-
/// Module initializer that configures ReactiveUI before any benchmarks run.
12+
/// One-shot initializer that configures ReactiveUI before any benchmarks run.
13+
/// Call <see cref="EnsureInitialized"/> from each benchmark class's static constructor.
1514
/// </summary>
1615
internal static class ModuleInitializer
1716
{
17+
private static int _initialized;
18+
1819
/// <summary>
19-
/// Initializes ReactiveUI using the builder pattern.
20+
/// Ensures ReactiveUI is initialized exactly once, regardless of how many
21+
/// benchmark classes call this method.
2022
/// </summary>
21-
[ModuleInitializer]
22-
internal static void Initialize()
23+
internal static void EnsureInitialized()
2324
{
25+
if (Interlocked.Exchange(ref _initialized, 1) != 0)
26+
{
27+
return;
28+
}
29+
2430
ModeDetector.OverrideModeDetector(new BenchmarkModeDetector());
2531
RxAppBuilder.CreateReactiveUIBuilder()
2632
.WithCoreServices()

src/benchmarks/ReactiveUI.Binding.Benchmarks.ReactiveUI/ReactiveUIBindingBenchmark.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class ReactiveUIBindingBenchmark
2424
private BenchmarkViewModel _source = null!;
2525
private BenchmarkView _target = null!;
2626

27+
static ReactiveUIBindingBenchmark() => ModuleInitializer.EnsureInitialized();
28+
2729
/// <summary>
2830
/// Sets up fresh source and target objects before each benchmark iteration.
2931
/// </summary>

0 commit comments

Comments
 (0)