diff --git a/Directory.Packages.props b/Directory.Packages.props
index b6f3b0bc..7077cf15 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,8 +23,12 @@
+
+
+
+
diff --git a/ReactiveProperty.slnx b/ReactiveProperty.slnx
index beeceb3c..d1bb9560 100644
--- a/ReactiveProperty.slnx
+++ b/ReactiveProperty.slnx
@@ -61,6 +61,9 @@
+
+
+
@@ -82,6 +85,9 @@
+
+
+
diff --git a/Source/ReactiveProperty.Analyzer/AnalyzerReleases.Shipped.md b/Source/ReactiveProperty.Analyzer/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000..f50bb1fe
--- /dev/null
+++ b/Source/ReactiveProperty.Analyzer/AnalyzerReleases.Shipped.md
@@ -0,0 +1,2 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
diff --git a/Source/ReactiveProperty.Analyzer/AnalyzerReleases.Unshipped.md b/Source/ReactiveProperty.Analyzer/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000..6ecf9793
--- /dev/null
+++ b/Source/ReactiveProperty.Analyzer/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,8 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+RP0001 | Usage | Warning | ReactivePropertyNullableAnalyzer
diff --git a/Source/ReactiveProperty.Analyzer/ReactiveProperty.Analyzer.csproj b/Source/ReactiveProperty.Analyzer/ReactiveProperty.Analyzer.csproj
new file mode 100644
index 00000000..ffabb180
--- /dev/null
+++ b/Source/ReactiveProperty.Analyzer/ReactiveProperty.Analyzer.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netstandard2.0
+ ReactiveProperty.Analyzer
+ Reactive.Bindings.Analyzer
+ Roslyn analyzer for ReactiveProperty that supports nullable reference types.
+ true
+ true
+
+ false
+ false
+ false
+ true
+ ..\ReactiveProperty.Core\key.snk
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/ReactiveProperty.Analyzer/ReactivePropertyNullableAnalyzer.cs b/Source/ReactiveProperty.Analyzer/ReactivePropertyNullableAnalyzer.cs
new file mode 100644
index 00000000..a818a63f
--- /dev/null
+++ b/Source/ReactiveProperty.Analyzer/ReactivePropertyNullableAnalyzer.cs
@@ -0,0 +1,199 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Reactive.Bindings.Analyzer;
+
+///
+/// Reports usages of ReactiveProperty family types for a non-nullable reference type that are
+/// created without an initial value. In that situation the Value property is initialized to
+/// null even though its type is a non-nullable reference type, which the C# compiler cannot
+/// detect on its own.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ReactivePropertyNullableAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ /// The diagnostic id reported by this analyzer.
+ ///
+ public const string DiagnosticId = "RP0001";
+
+ private const string Category = "Usage";
+
+ private static readonly DiagnosticDescriptor s_rule = new(
+ DiagnosticId,
+ "Provide an initial value or use a nullable type argument",
+ "'{0}' is created for the non-nullable reference type '{1}' without an initial value, so its Value will be null. Provide an initial value or use a nullable type argument such as '{1}?'.",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "ReactiveProperty family types initialize Value to default (null for reference types) when no initial value is supplied. The C# compiler cannot detect this, so this analyzer reports it for non-nullable reference types.");
+
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(s_rule);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterCompilationStartAction(OnCompilationStart);
+ }
+
+ private static void OnCompilationStart(CompilationStartAnalysisContext context)
+ {
+ var compilation = context.Compilation;
+
+ var targetTypes = new[]
+ {
+ compilation.GetTypeByMetadataName("Reactive.Bindings.ReactiveProperty`1"),
+ compilation.GetTypeByMetadataName("Reactive.Bindings.ReactivePropertySlim`1"),
+ compilation.GetTypeByMetadataName("Reactive.Bindings.ReadOnlyReactivePropertySlim`1"),
+ compilation.GetTypeByMetadataName("Reactive.Bindings.ReadOnlyReactiveProperty`1"),
+ }
+ .Where(x => x is not null)
+ .Select(x => (ISymbol?)x!.OriginalDefinition)
+ .ToImmutableHashSet(SymbolEqualityComparer.Default);
+
+ if (targetTypes.IsEmpty)
+ {
+ return;
+ }
+
+ context.RegisterOperationAction(ctx => AnalyzeObjectCreation(ctx, targetTypes), OperationKind.ObjectCreation);
+ context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
+ }
+
+ private static void AnalyzeObjectCreation(OperationAnalysisContext context, ImmutableHashSet targetTypes)
+ {
+ var operation = (IObjectCreationOperation)context.Operation;
+ if (operation.Type is not INamedTypeSymbol createdType || createdType.TypeArguments.Length != 1)
+ {
+ return;
+ }
+
+ if (!targetTypes.Contains(createdType.OriginalDefinition))
+ {
+ return;
+ }
+
+ var typeArgument = createdType.TypeArguments[0];
+ if (!IsNonNullableReferenceType(typeArgument) || HasInitialValue(operation.Arguments))
+ {
+ return;
+ }
+
+ Report(context, operation.Syntax.GetLocation(), createdType, typeArgument);
+ }
+
+ private static void AnalyzeInvocation(OperationAnalysisContext context)
+ {
+ var operation = (IInvocationOperation)context.Operation;
+ var method = operation.TargetMethod;
+ if (method.Name is not ("ToReadOnlyReactivePropertySlim" or "ToReadOnlyReactiveProperty"))
+ {
+ return;
+ }
+
+ if (method.ContainingNamespace?.ToDisplayString() != "Reactive.Bindings")
+ {
+ return;
+ }
+
+ if (operation.Type is not INamedTypeSymbol returnType)
+ {
+ return;
+ }
+
+ // The compiler infers the return type argument as nullable because of the
+ // 'initialValue = default' parameter, so the element type is taken from the source
+ // observable instead, which reflects what the caller actually provided.
+ ITypeSymbol? typeArgument = null;
+ foreach (var argument in operation.Arguments)
+ {
+ typeArgument = GetObservableElementType(argument.Value.Type);
+ if (typeArgument is not null)
+ {
+ break;
+ }
+ }
+
+ if (typeArgument is null || !IsNonNullableReferenceType(typeArgument) || HasInitialValue(operation.Arguments))
+ {
+ return;
+ }
+
+ Report(context, operation.Syntax.GetLocation(), returnType, typeArgument);
+ }
+
+ private static ITypeSymbol? GetObservableElementType(ITypeSymbol? type)
+ {
+ if (type is null)
+ {
+ return null;
+ }
+
+ if (type is INamedTypeSymbol named &&
+ named.OriginalDefinition.SpecialType == SpecialType.None &&
+ named.OriginalDefinition.ToDisplayString() == "System.IObservable" &&
+ named.TypeArguments.Length == 1)
+ {
+ return named.TypeArguments[0];
+ }
+
+ foreach (var @interface in type.AllInterfaces)
+ {
+ if (@interface.OriginalDefinition.ToDisplayString() == "System.IObservable" &&
+ @interface.TypeArguments.Length == 1)
+ {
+ return @interface.TypeArguments[0];
+ }
+ }
+
+ return null;
+ }
+
+ private static void Report(OperationAnalysisContext context, Location location, INamedTypeSymbol type, ITypeSymbol typeArgument)
+ {
+ var diagnostic = Diagnostic.Create(
+ s_rule,
+ location,
+ type.OriginalDefinition.Name,
+ typeArgument.ToDisplayString());
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ private static bool HasInitialValue(ImmutableArray arguments)
+ {
+ foreach (var argument in arguments)
+ {
+ if (argument.Parameter?.Name == "initialValue")
+ {
+ return argument.ArgumentKind != ArgumentKind.DefaultValue;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsNonNullableReferenceType(ITypeSymbol typeSymbol)
+ {
+ // Skip open type parameters (e.g. usages inside generic code) to avoid false positives.
+ if (typeSymbol.TypeKind == TypeKind.TypeParameter)
+ {
+ return false;
+ }
+
+ // Only reference types can be null.
+ if (!typeSymbol.IsReferenceType)
+ {
+ return false;
+ }
+
+ // NotAnnotated: nullable context is enabled and the type is non-nullable (e.g. string).
+ // Annotated (string?) and None (nullable context disabled) are intentionally ignored.
+ return typeSymbol.NullableAnnotation == NullableAnnotation.NotAnnotated;
+ }
+}
diff --git a/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj b/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj
index d1a24f32..7144818a 100644
--- a/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj
+++ b/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj
@@ -12,4 +12,19 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj b/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj
index a536fd0c..c805d19d 100644
--- a/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj
+++ b/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj
@@ -16,6 +16,18 @@
+
+
+
+
+
+
+
diff --git a/Test/ReactiveProperty.Analyzer.Tests/AnalyzerVerifier.cs b/Test/ReactiveProperty.Analyzer.Tests/AnalyzerVerifier.cs
new file mode 100644
index 00000000..f58f52e1
--- /dev/null
+++ b/Test/ReactiveProperty.Analyzer.Tests/AnalyzerVerifier.cs
@@ -0,0 +1,68 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace ReactiveProperty.Analyzer.Tests;
+
+internal static class AnalyzerVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ public static DiagnosticResult Diagnostic(string diagnosticId) =>
+ CSharpAnalyzerVerifier.Diagnostic(diagnosticId);
+
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ };
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+
+ private sealed class Test : CSharpAnalyzerTest
+ {
+ public Test()
+ {
+ ReferenceAssemblies = new ReferenceAssemblies(
+ "net9.0",
+ new PackageIdentity("Microsoft.NETCore.App.Ref", "9.0.0"),
+ Path.Combine("ref", "net9.0"));
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId)!;
+ var parseOptions = ((CSharpParseOptions)project.ParseOptions!).WithLanguageVersion(LanguageVersion.Latest);
+ var compilationOptions = ((CSharpCompilationOptions)project.CompilationOptions!)
+ .WithNullableContextOptions(NullableContextOptions.Enable);
+
+ solution = solution
+ .WithProjectParseOptions(projectId, parseOptions)
+ .WithProjectCompilationOptions(projectId, compilationOptions);
+
+ foreach (var reference in ReactivePropertyReferences.Value)
+ {
+ solution = solution.AddMetadataReference(projectId, reference);
+ }
+
+ return solution;
+ });
+ }
+ }
+
+ private static readonly Lazy> ReactivePropertyReferences = new(() =>
+ {
+ var assemblies = new[]
+ {
+ typeof(Reactive.Bindings.ReactivePropertySlim<>).Assembly,
+ typeof(Reactive.Bindings.ReactiveProperty<>).Assembly,
+ typeof(System.Reactive.Linq.Observable).Assembly,
+ };
+
+ return assemblies
+ .Select(x => (MetadataReference)MetadataReference.CreateFromFile(x.Location))
+ .ToImmutableArray();
+ });
+}
diff --git a/Test/ReactiveProperty.Analyzer.Tests/GlobalUsings.cs b/Test/ReactiveProperty.Analyzer.Tests/GlobalUsings.cs
new file mode 100644
index 00000000..974ab121
--- /dev/null
+++ b/Test/ReactiveProperty.Analyzer.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
diff --git a/Test/ReactiveProperty.Analyzer.Tests/ReactiveProperty.Analyzer.Tests.csproj b/Test/ReactiveProperty.Analyzer.Tests/ReactiveProperty.Analyzer.Tests.csproj
new file mode 100644
index 00000000..218ef6cc
--- /dev/null
+++ b/Test/ReactiveProperty.Analyzer.Tests/ReactiveProperty.Analyzer.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net9.0
+ ReactiveProperty.Analyzer.Tests
+ false
+ enable
+ enable
+ 14.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Test/ReactiveProperty.Analyzer.Tests/ReactivePropertyNullableAnalyzerTest.cs b/Test/ReactiveProperty.Analyzer.Tests/ReactivePropertyNullableAnalyzerTest.cs
new file mode 100644
index 00000000..44c41b29
--- /dev/null
+++ b/Test/ReactiveProperty.Analyzer.Tests/ReactivePropertyNullableAnalyzerTest.cs
@@ -0,0 +1,225 @@
+using Microsoft.CodeAnalysis.Testing;
+using Reactive.Bindings.Analyzer;
+using Verify = ReactiveProperty.Analyzer.Tests.AnalyzerVerifier;
+
+namespace ReactiveProperty.Analyzer.Tests;
+
+[TestClass]
+public class ReactivePropertyNullableAnalyzerTest
+{
+ [TestMethod]
+ public async Task ReactiveProperty_NonNullableReferenceType_WithoutInitialValue_Reports()
+ {
+ var source = """
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = {|#0:new ReactiveProperty()|};
+ }
+ }
+ """;
+ var expected = Verify.Diagnostic(ReactivePropertyNullableAnalyzer.DiagnosticId)
+ .WithLocation(0)
+ .WithArguments("ReactiveProperty", "string");
+ await Verify.VerifyAnalyzerAsync(source, expected);
+ }
+
+ [TestMethod]
+ public async Task ReactivePropertySlim_NonNullableReferenceType_WithoutInitialValue_Reports()
+ {
+ var source = """
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = {|#0:new ReactivePropertySlim()|};
+ }
+ }
+ """;
+ var expected = Verify.Diagnostic(ReactivePropertyNullableAnalyzer.DiagnosticId)
+ .WithLocation(0)
+ .WithArguments("ReactivePropertySlim", "string");
+ await Verify.VerifyAnalyzerAsync(source, expected);
+ }
+
+ [TestMethod]
+ public async Task ReactivePropertySlim_WithModeButNoInitialValue_Reports()
+ {
+ var source = """
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = {|#0:new ReactivePropertySlim(mode: ReactivePropertyMode.None)|};
+ }
+ }
+ """;
+ var expected = Verify.Diagnostic(ReactivePropertyNullableAnalyzer.DiagnosticId)
+ .WithLocation(0)
+ .WithArguments("ReactivePropertySlim", "string");
+ await Verify.VerifyAnalyzerAsync(source, expected);
+ }
+
+ [TestMethod]
+ public async Task ReactivePropertySlim_WithInitialValue_DoesNotReport()
+ {
+ var source = """
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = new ReactivePropertySlim("initial");
+ }
+ }
+ """;
+ await Verify.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ReactivePropertySlim_NullableReferenceType_DoesNotReport()
+ {
+ var source = """
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = new ReactivePropertySlim();
+ }
+ }
+ """;
+ await Verify.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ReactivePropertySlim_ValueType_DoesNotReport()
+ {
+ var source = """
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = new ReactivePropertySlim();
+ }
+ }
+ """;
+ await Verify.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ReactivePropertySlim_NullableContextDisabled_DoesNotReport()
+ {
+ var source = """
+ #nullable disable
+ using Reactive.Bindings;
+ class C
+ {
+ void M()
+ {
+ var x = new ReactivePropertySlim();
+ }
+ }
+ """;
+ await Verify.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ReadOnlyReactivePropertySlim_Constructor_NonNullableReferenceType_Reports()
+ {
+ var source = """
+ using System;
+ using Reactive.Bindings;
+ class C
+ {
+ void M(IObservable source)
+ {
+ var x = {|#0:new ReadOnlyReactivePropertySlim(source)|};
+ }
+ }
+ """;
+ var expected = Verify.Diagnostic(ReactivePropertyNullableAnalyzer.DiagnosticId)
+ .WithLocation(0)
+ .WithArguments("ReadOnlyReactivePropertySlim", "string");
+ await Verify.VerifyAnalyzerAsync(source, expected);
+ }
+
+ [TestMethod]
+ public async Task ToReadOnlyReactivePropertySlim_NonNullableReferenceType_WithoutInitialValue_Reports()
+ {
+ var source = """
+ using System;
+ using Reactive.Bindings;
+ class C
+ {
+ void M(IObservable source)
+ {
+ var x = {|#0:source.ToReadOnlyReactivePropertySlim()|};
+ }
+ }
+ """;
+ var expected = Verify.Diagnostic(ReactivePropertyNullableAnalyzer.DiagnosticId)
+ .WithLocation(0)
+ .WithArguments("ReadOnlyReactivePropertySlim", "string");
+ await Verify.VerifyAnalyzerAsync(source, expected);
+ }
+
+ [TestMethod]
+ public async Task ToReadOnlyReactivePropertySlim_WithInitialValue_DoesNotReport()
+ {
+ var source = """
+ using System;
+ using Reactive.Bindings;
+ class C
+ {
+ void M(IObservable source)
+ {
+ var x = source.ToReadOnlyReactivePropertySlim("initial");
+ }
+ }
+ """;
+ await Verify.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ToReadOnlyReactivePropertySlim_NullableReferenceTypeSource_DoesNotReport()
+ {
+ var source = """
+ using System;
+ using Reactive.Bindings;
+ class C
+ {
+ void M(IObservable source)
+ {
+ var x = source.ToReadOnlyReactivePropertySlim();
+ }
+ }
+ """;
+ await Verify.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ToReadOnlyReactiveProperty_NonNullableReferenceType_WithoutInitialValue_Reports()
+ {
+ var source = """
+ using System;
+ using Reactive.Bindings;
+ class C
+ {
+ void M(IObservable source)
+ {
+ var x = {|#0:source.ToReadOnlyReactiveProperty()|};
+ }
+ }
+ """;
+ var expected = Verify.Diagnostic(ReactivePropertyNullableAnalyzer.DiagnosticId)
+ .WithLocation(0)
+ .WithArguments("ReadOnlyReactiveProperty", "string");
+ await Verify.VerifyAnalyzerAsync(source, expected);
+ }
+}
diff --git a/dev-docs/adr/0006-reactiveproperty-nullable-analyzer.md b/dev-docs/adr/0006-reactiveproperty-nullable-analyzer.md
new file mode 100644
index 00000000..4287c85b
--- /dev/null
+++ b/dev-docs/adr/0006-reactiveproperty-nullable-analyzer.md
@@ -0,0 +1,61 @@
+# 0006. Ship a Roslyn analyzer (RP0001) for nullable reference type usage
+
+- **Status:** Accepted
+- **Date:** 2026-07-08
+- **Deciders:** ReactiveProperty maintainers
+
+## Context
+
+ReactiveProperty enables nullable reference types (`enable`), but the C#
+compiler cannot detect one important source of `null`: the `Value` of a `ReactiveProperty`,
+`ReactivePropertySlim`, or `ReadOnlyReactivePropertySlim` is initialized to `default(T)`
+when no initial value is supplied. For a non-nullable reference type such as `string`, this
+leaves `Value` as `null` even though its declared type says it can never be `null`.
+
+For backward compatibility the parameterless / `initialValue = default` constructors must stay,
+so the type system alone cannot express "you got a null here". This was tracked in issue #241,
+where the community converged on a Roslyn analyzer as the way to add nullability support without a
+breaking change. The compiler additionally infers the return type of
+`ToReadOnlyReactivePropertySlim` / `ToReadOnlyReactiveProperty` as nullable (because of the
+`initialValue = default` parameter), so the analyzer cannot rely on the inferred return type and
+must inspect the source `IObservable` element type instead.
+
+## Decision
+
+We will add a `Reactive.Bindings.Analyzer.ReactivePropertyNullableAnalyzer` in a new
+`Source/ReactiveProperty.Analyzer` project (netstandard2.0). It reports diagnostic **RP0001**
+(category `Usage`, severity `Warning`) when:
+
+- `new ReactiveProperty(...)`, `new ReactivePropertySlim(...)`, or
+ `new ReadOnlyReactivePropertySlim(...)` is created without an `initialValue`, or
+- `source.ToReadOnlyReactivePropertySlim(...)` / `source.ToReadOnlyReactiveProperty(...)` is
+ called without an `initialValue`,
+
+**and** the relevant type argument `T` is a non-nullable reference type (nullable context enabled,
+annotation `NotAnnotated`). Value types, nullable annotated types (`T?`), open type parameters,
+and code where the nullable context is disabled are ignored.
+
+The analyzer is packed into both the `ReactiveProperty.Core` and `ReactiveProperty` NuGet packages
+under `analyzers/dotnet/cs`, so consumers of either package get it. It is only referenced with
+`ReferenceOutputAssembly="false"` so it is not a runtime dependency.
+
+### Alternatives considered
+
+- **Do nothing / wait for the compiler** — the compiler still cannot detect this pattern and the
+ request in #241 remained open for years; rejected.
+- **Add `[Obsolete]` to the default constructor** — rejected in #241 because it forces
+ `new ReactiveProperty(null)` and hurts the developer experience for the legitimate
+ `new ReactiveProperty()` usage.
+- **Trust the inferred return type of the `ToReadOnly*` extension methods** — rejected because the
+ compiler infers it as nullable (`string?`) due to `initialValue = default`; the source
+ observable's element type is used instead.
+
+## Consequences
+
+- Consumers using nullable reference types get a clear, actionable warning at the exact call site,
+ with the fix spelled out (provide an initial value or use `T?`).
+- No public API or runtime behavior changes; the analyzer only produces diagnostics.
+- Two new projects are added: the analyzer and its MSTest test project (using
+ `Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.MSTest`).
+- Future rules (for example, dedicated codefixes) can be added to the same analyzer package and
+ tracked via `AnalyzerReleases.Unshipped.md` / `AnalyzerReleases.Shipped.md`.
diff --git a/dev-docs/adr/README.md b/dev-docs/adr/README.md
index e79bb773..3179aa8b 100644
--- a/dev-docs/adr/README.md
+++ b/dev-docs/adr/README.md
@@ -39,3 +39,4 @@ Trivial, easily reversible choices (local refactors, naming) do not need an ADR.
| [0003](./0003-reactiveproperty-r3-migration-bridge.md) | Ship a permanent minimal `ReactiveProperty.R3` migration bridge and a migration skill | Accepted |
| [0004](./0004-reactiveproperty-r3-wpf-event-to-reactive.md) | Provide R3-targeting EventToReactive trigger actions in a new ReactiveProperty.R3.WPF package | Accepted |
| [0005](./0005-remove-vuepress-plain-markdown-docs.md) | Remove VuePress; publish docs as plain Markdown on GitHub | Accepted |
+| [0006](./0006-reactiveproperty-nullable-analyzer.md) | Ship a Roslyn analyzer (RP0001) for nullable reference type usage | Accepted |