RefactorMCP catalog

Convert to Nullable-Aware

Enables nullable reference types in one file and adds the ? annotations its code shows it needs, so the file compiles without new warnings. Files are converted one at a time, so a project can move to nullable gradually.

This is a generator: the annotations are the ones this file's flow requires, chosen by following the compiler's warnings, not the only possible set; a person might instead initialise a field or add a null check. The after/ fixtures pin that choice. Run-time behaviour does not change; the annotations only inform the compiler.

The target is the file: "target": { "file": "Directory.cs" }.

Precondition

  • The file has no #nullable directive and its project does not already enable nullable reference types.
  • Every nullable warning that enabling produces can be answered by annotating a declaration in the file (see below).

Transformation

  • #nullable enable becomes the first line, followed by a blank line and the file as it was, including any leading comment.
  • The file is compiled and each new warning is answered by annotating one declaration with ?, repeating until no warning remains:

| Warning | Annotated | |---|---| | CS8600, CS8601, CS8625: a possibly null value assigned or initialised | the local, field, property or parameter it is stored in | | CS8604, CS8625: a possibly null argument | the parameter it is passed to | | CS8603: a possibly null return | the method's, property's or local function's return type | | CS8618: a member left unset by the constructor | the field, property or event | | CS8765, CS8767: a parameter less nullable than the member it overrides or implements | the parameter |

So a null followed through a return, a local and an argument annotates each declaration in turn.

  • Only reference types and unconstrained type parameters (T?) are annotated, and only when written explicitly: var locals are already inferred nullable.

Preserved

  • Behaviour, and every other file: callers in files that are still nullable-oblivious see no warnings and are not changed.

Limitations

  • Annotations are only added, never removed, and !, null checks and [NotNull]-style attributes are never introduced.
  • A field declaration naming several variables is annotated as a whole.
  • Warnings about generic type arguments (List<string> holding null), delegates, or members declared in other files are not answered, so the conversion is refused.

Error codes

CodeMeaning
already-nullable-awarethe file has a #nullable directive or its project enables nullable
unresolved-warninga warning remains that no annotation fixes, such as CS8602, a dereference of a possibly null value

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#already-nullable-aware

Refuses a file that already sets its nullable context

refusal
TargetGreeter.cs
Refusesalready-nullable-aware; every file is left unchanged
Greeter.csinput
1 #nullable enable
2
3 namespace Shop
4 {
5 public class Greeter
6 {
7 public string Greet(string name) => "Hello " + name;
8 }
9 }

#constructor-and-event

Annotates a field and an event the constructor leaves unset, and leaves the fields it sets

success
TargetBasket.cs
Basket.csmodified
1+#nullable enable
2+
13 using System;
24 using System.Collections.Generic;
35
46 namespace Shop
57 {
68 public class Basket
79 {
810 private readonly List<string> _items;
9− private string _coupon;
11+ private string? _coupon;
1012
1113 public Basket(IEnumerable<string> items)
1214 {
1315 _items = new List<string>(items);
1416 }
1517
16− public event Action Changed;
18+ public event Action? Changed;
1719
1820 public void Apply(string coupon)
1921 {
2022 _coupon = coupon;
2123 Changed?.Invoke();
2224 }
2325
2426 public int CouponLength => _coupon?.Length ?? 0;
2527
2628 public int Count => _items.Count;
2729 }
2830 }

#generic

Annotates an unconstrained type parameter that may hold its default value

success
TargetCache.cs
Cache.csmodified
1+#nullable enable
2+
13 using System.Collections.Generic;
24
35 namespace Shop
46 {
57 public class Cache<T>
68 {
79 private readonly Dictionary<string, T> _items = new Dictionary<string, T>();
810
9− public T Get(string key)
11+ public T? Get(string key)
1012 {
1113 return _items.TryGetValue(key, out var value) ? value : default;
1214 }
1315 }
1416 }

#null-flows

Enables nullable in the file and annotates the field, return and parameter that hold null; a file calling it that is not converted is left alone

success
TargetDirectory.cs
Directory.csmodified
1+#nullable enable
2+
13 using System.Collections.Generic;
24
35 namespace Shop
46 {
57 public class Directory
68 {
79 private readonly Dictionary<string, string> _emails = new Dictionary<string, string>();
8− private string _lastLookup;
10+ private string? _lastLookup;
911
10− public string LastLookup => _lastLookup;
12+ public string? LastLookup => _lastLookup;
1113
12− public string Find(string name)
14+ public string? Find(string name)
1315 {
1416 _lastLookup = name;
1517 if (_emails.TryGetValue(name, out var email))
1618 return email;
1719
1820 return null;
1921 }
2022
21− public string Describe(string name, string fallback = null)
23+ public string Describe(string name, string? fallback = null)
2224 {
2325 var email = Find(name);
2426 return email ?? fallback ?? "unknown";
2527 }
2628 }
2729 }
Report.csunchanged
11 namespace Shop
22 {
33 public class Report
44 {
55 public int Length(Directory directory) => directory.Find("ann").Length;
66 }
77 }

#overrides-and-comments

Matches the nullability of an overridden parameter, and puts the directive above a leading comment

success
TargetMoney.cs
Money.csmodified
1+#nullable enable
2+
13 // Money values used across the shop.
24 namespace Shop
35 {
46 public class Money
57 {
68 public Money(decimal amount)
79 {
810 Amount = amount;
911 }
1012
1113 public decimal Amount { get; }
1214
13− public override bool Equals(object obj) => obj is Money other && other.Amount == Amount;
15+ public override bool Equals(object? obj) => obj is Money other && other.Amount == Amount;
1416
1517 public override int GetHashCode() => Amount.GetHashCode();
1618 }
1719 }

#possible-null-dereference

Refuses when a value that may be null is dereferenced, which no annotation can fix

refusal
TargetDirectory.cs
Refusesunresolved-warning mentioning “CS8602”; every file is left unchanged
Directory.csinput
1 namespace Shop
2 {
3 public class Directory
4 {
5 private string _owner;
6
7 public void Claim(string owner)
8 {
9 _owner = owner;
10 }
11
12 public int OwnerLength() => _owner.Length;
13 }
14 }

#project-nullable

Refuses a file in a project that already enables nullable

refusal
TargetGreeter.cs
Projectnullable enable
Refusesalready-nullable-aware; every file is left unchanged
Greeter.csinput
1 namespace Shop
2 {
3 public class Greeter
4 {
5 public string Greet(string name) => "Hello " + name;
6 }
7 }

#propagation

Follows a null through a return, an explicitly typed local and an argument, annotating each declaration in turn

success
TargetLabels.cs
Labels.csmodified
1+#nullable enable
2+
13 using System;
24
35 namespace Shop
46 {
57 public class Labels
68 {
79 public void Print(int code)
810 {
9− string title = Lookup(code);
11+ string? title = Lookup(code);
1012 Write(title);
1113 }
1214
13− private static string Lookup(int code)
15+ private static string? Lookup(int code)
1416 {
1517 return code == 0 ? null : "Code " + code;
1618 }
1719
18− private static void Write(string text)
20+ private static void Write(string? text)
1921 {
2022 Console.WriteLine(text ?? "(none)");
2123 }
2224 }
2325 }