RefactorMCP catalog

Add Null Checks

Adds an argument null guard at the start of a method or constructor for each reference-type parameter that is meant never to be null.

This is a generator: it changes behaviour, because a call that passed null now throws ArgumentNullException at the start of the method instead of failing later or not at all. The after/ fixtures pin one design, described below, rather than the only correct answer.

The target is the method or constructor, by symbol: "target": { "symbol": "M:Shop.Order.#ctor(Shop.Customer,System.String)" }.

Precondition

  • The method or constructor has a body, block or expression.
  • At least one parameter needs a guard: a reference type (including a type parameter constrained to a class) that is not out, not annotated as nullable, does not default to null, and is not already guarded.

Transformation

  • Each parameter needing one gets ArgumentNullException.ThrowIfNull(name);, in parameter order, at the top of the body, followed by a blank line before the first existing statement.
  • A parameter counts as already guarded when the body calls ArgumentNullException.ThrowIfNull on it, throws in an if whose condition is p == null, null == p or p is null, or uses p ?? throw.
  • An expression body becomes a block: the expression is returned, or becomes a statement when the method returns nothing (void, a constructor, or an async method returning Task or ValueTask).
  • using System; is added when ArgumentNullException does not bind where the guards go.

Preserved

  • The existing statements, with the comments and blank lines around them; a comment above the first statement stays with it, below the guards.
  • Behaviour for every call that passes non-null arguments.

Limitations

  • In a constructor the guards run after the base(...) or this(...) call, which may already have used the parameter.
  • In an iterator or async method the guard runs when iteration or the task starts, not at the call, as for any statement in such a body.
  • Unconstrained type parameters are not guarded, since they may be value types; nor are parameters annotated nullable, which are declared to accept null.
  • Existing guards in other shapes, such as a helper method, are not recognised, so their parameters are guarded again.
  • Local functions, lambdas, property setters and indexers are not covered.

Error codes

CodeMeaning
no-bodythe method is abstract, extern, partial without a body, or an interface member
nothing-to-guardno parameter is a reference type left unguarded

Cases

·

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

#comments-and-existing-guard

Leaves a parameter the method already guards, and keeps the comment above the first statement with that statement

success
Targetsymbol M:Shop.Mailer.Send(System.String,System.String,System.String)
Mailer.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Mailer
77 {
88 private readonly List<string> _sent = new List<string>();
99
1010 /// <summary>Queues a message.</summary>
1111 public void Send(string to, string subject, string body)
1212 {
13+ ArgumentNullException.ThrowIfNull(subject);
14+ ArgumentNullException.ThrowIfNull(body);
15+
1316 // Normalise the address first
1417 if (to == null)
1518 throw new ArgumentNullException(nameof(to));
1619
1720 _sent.Add(to.Trim() + ": " + subject + body); // record it
1821 }
1922 }
2023 }

#constructor

Guards a constructor's parameters; the guards run after the base constructor call

success
Targetsymbol M:Shop.Order.#ctor(Shop.Customer,System.String)
Order.csmodified
1+using System;
2+
13 namespace Shop
24 {
35 public class Customer
46 {
57 }
68
79 public class Entity
810 {
911 protected Entity(string id)
1012 {
1113 Id = id;
1214 }
1315
1416 public string Id { get; }
1517 }
1618
1719 public class Order : Entity
1820 {
1921 private readonly Customer _customer;
2022
2123 public Order(Customer customer, string id) : base(id)
2224 {
25+ ArgumentNullException.ThrowIfNull(customer);
26+ ArgumentNullException.ThrowIfNull(id);
27+
2328 _customer = customer;
2429 }
2530 }
2631 }

#expression-bodied

Gives an expression-bodied method a block body so the guard can precede the returned expression

success
Targetsymbol M:Shop.Text.Shout(System.String)
Text.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public static class Text
66 {
7− public static string Shout(string text) => text.ToUpperInvariant() + "!";
7+ public static string Shout(string text)
8+ {
9+ ArgumentNullException.ThrowIfNull(text);
10+
11+ return text.ToUpperInvariant() + "!";
12+ }
813
914 public static void Log(string text) => Console.WriteLine(text);
1015 }
1116 }

#generic-overload

Guards a type parameter constrained to a class but not an unconstrained one, and only in the chosen overload

success
Targetsymbol M:Shop.Cache.Put``2(System.String,``0,``1)
Cache.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Cache
77 {
88 private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
99
1010 public void Put(string key, object value)
1111 {
1212 _items[key] = value;
1313 }
1414
1515 public void Put<TValue, TTag>(string key, TValue value, TTag tag) where TValue : class
1616 {
17+ ArgumentNullException.ThrowIfNull(key);
18+ ArgumentNullException.ThrowIfNull(value);
19+
1720 _items[key] = value;
1821 _items[key + "#tag"] = tag;
1922 }
2023 }
2124 }

#no-body

Refuses an abstract method, which has no body to guard

refusal
Targetsymbol M:Shop.Shape.Describe(System.String)
Refusesno-body; every file is left unchanged
Shape.csinput
1 namespace Shop
2 {
3 public abstract class Shape
4 {
5 public abstract string Describe(string prefix);
6 }
7 }

#nothing-to-guard

Refuses a method whose parameters are all value types

refusal
Targetsymbol M:Shop.Maths.Add(System.Int32,System.Int32)
Refusesnothing-to-guard; every file is left unchanged
Maths.csinput
1 namespace Shop
2 {
3 public static class Maths
4 {
5 public static int Add(int left, int right)
6 {
7 return left + right;
8 }
9 }
10 }

#nullable-annotations

In a nullable context, parameters annotated as nullable accept null and are not guarded

success
Targetsymbol M:Shop.Greeter.Greet(System.String,System.String)
Projectnullable enable
Greeter.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Greeter
66 {
77 public string Greet(string name, string? title)
88 {
9+ ArgumentNullException.ThrowIfNull(name);
10+
911 return title is null ? "Hello " + name : "Hello " + title + " " + name;
1012 }
1113 }
1214 }

#reference-parameters

Guards each reference-type parameter, skipping value types and parameters that default to null, and imports System

success
Targetsymbol M:Shop.Printer.Print(Shop.Customer,System.Int32,System.String,System.String)
Printer.csmodified
1+using System;
2+
13 namespace Shop
24 {
35 public class Customer
46 {
57 public string Name { get; set; }
68 }
79
810 public class Printer
911 {
1012 public string Print(Customer customer, int copies, string heading, string footer = null)
1113 {
14+ ArgumentNullException.ThrowIfNull(customer);
15+ ArgumentNullException.ThrowIfNull(heading);
16+
1217 return heading + customer.Name + copies + footer;
1318 }
1419 }
1520 }

#void-expression-bodied

A void method's expression body becomes a statement after the guard

success
Targetsymbol M:Shop.Text.Log(System.String)
Text.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public static class Text
66 {
77 public static string Shout(string text) => text.ToUpperInvariant() + "!";
88
9− public static void Log(string text) => Console.WriteLine(text);
9+ public static void Log(string text)
10+ {
11+ ArgumentNullException.ThrowIfNull(text);
12+
13+ Console.WriteLine(text);
14+ }
1015 }
1116 }