RefactorMCP catalog

Replace Temp with Query

Replaces a local variable that holds the result of an expression with a private method that computes it, and calls the method wherever the local was read.

Recipe

  1. extract-method on the local's initializer, selected with markers, with name the query's name. The initializer is a single expression, so the new method returns its value and the declaration now calls it.
  2. inline-local-variable on the local, targeted by the containing method's symbol with arguments.local naming it. Each read becomes the call and the declaration goes.
"steps": [
  { "refactoring": "extract-method", "target": { "file": "Order.cs", "selection": "marker" }, "arguments": { "name": "BasePrice" } },
  { "refactoring": "inline-local-variable", "target": { "symbol": "M:Shop.Order.Price" }, "arguments": { "local": "basePrice" } }
]

Inline Local Variable treats a call as a side effect, so the recipe only completes when the local is read once. The dedicated implementation checks the extracted expression rather than the call, and so also replaces a local read several times.

Arguments

ArgumentMeaning
namethe name of the query method

The target is the local, by a caret on its declaration or on any read of it, or in a later step of a composite by the containing method's symbol with arguments.local naming it.

Precondition

  • The local is declared with an initializer by a local declaration statement in a block, in the body of a class's method. It is not a using declaration or a ref local.
  • The local is never written after its declaration, and is read at least once.
  • When it is read more than once, the initializer has no side effects: no call, object creation, assignment, increment or await.
  • No local, parameter or field the initializer reads is assigned between the declaration and a read.
  • The class has no member with the query's name.

Transformation

  • A private method with the given name is added after the containing method, returning the initializer. It is static when the containing method is, and takes the locals and parameters the initializer reads, in the order it first reads them.
  • The query returns the local's declared type, so a conversion the declaration made, such as double total = count * weight; from int operands, is still made.
  • Each read of the local becomes a call of the query, and the declaration is removed.

Preserved

  • The value at every read, since nothing the initializer reads changes before it.
  • Comments above the declaration stay in place, above the statement that followed it; comments inside the initializer move into the query.

Limitations

  • The query is evaluated at each read instead of once. Calls inside the initializer are refused when the local is read more than once, but state a called method reads is not checked for changes between the declaration and a single read.
  • Locals in constructors, accessors, local functions and lambdas, and in structs and records, are not covered.

Error codes

CodeMeaning
no-initializerthe local has no initializer to become the query
assigned-after-declarationthe local is written after its declaration
initializer-has-side-effectsthe initializer has side effects and the local is read more than once
initializer-inputs-changea variable the initializer reads is assigned before a read
never-usedthe local is never read
name-conflictthe class already has a member with the query's name
not-in-methodthe local is not in the body of a class's method
not-a-localthe target is not a local variable

Cases

·

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

#assigned-after-declaration-rejected

Refuses a temp that is assigned again, since a query cannot stand for more than one value

refusal
TargetOrder.cs, the /*^*/ caret
Arguments
name"Price"
Refusesassigned-after-declaration mentioning “'price'”; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private decimal _base;
6 private bool _member;
7
8 public decimal Charge()
9 {
10 decimal /*^*/price = _base;
11 if (_member)
12 price = price * 0.9m;
13 return price;
14 }
15 }
16 }

#in-accessor-rejected

Refuses a temp in a property accessor, where Extract Method cannot create the query

refusal
TargetOrder.cs, the /*^*/ caret
Arguments
name"BasePrice"
Refusesnot-in-method; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private int _quantity;
6 private decimal _itemPrice;
7
8 public decimal Price
9 {
10 get
11 {
12 decimal /*^*/basePrice = _quantity * _itemPrice;
13 return basePrice * 0.98m;
14 }
15 }
16 }
17 }

#inputs-change-rejected

Refuses when a parameter the initializer reads is assigned before the temp is used, since the query would see the new value

refusal
TargetCounter.cs, the /*^*/ caret
Arguments
name"Doubled"
Refusesinitializer-inputs-change mentioning “'count'”; every file is left unchanged
Counter.csinput
1 namespace Shop
2 {
3 public class Counter
4 {
5 public int Next(int count)
6 {
7 int /*^*/doubled = count * 2;
8 count++;
9 return doubled + count;
10 }
11 }
12 }

#keeps-comments

A comment above the temp stays where it was, and a comment inside the initializer moves into the query

success
TargetBasket.cs, the /*^*/ caret
Arguments
name"Subtotal"
Basket.csmodified
11 namespace Shop
22 {
33 public class Basket
44 {
55 private decimal _price;
66 private int _count;
77
88 public decimal Total()
99 {
1010 // Everything in the basket, before tax.
11− decimal /*^*/subtotal = _price /* per item */ * _count;
1211
1312 // Tax is charged on the subtotal.
14− return subtotal * 1.2m;
13+ return Subtotal() * 1.2m;
14+ }
15+
16+ private decimal Subtotal()
17+ {
18+ return _price /* per item */ * _count;
1519 }
1620 }
1721 }

#keeps-declared-type

The query returns the temp's declared type, so a conversion the declaration made still happens, and a parameter the initializer reads is passed to it

success
TargetStock.cs, the /*^*/ caret
Arguments
name"TotalWeight"
Stock.csmodified
11 namespace Shop
22 {
33 public class Stock
44 {
55 private readonly int _unitWeight;
66
77 public Stock(int unitWeight)
88 {
99 _unitWeight = unitWeight;
1010 }
1111
1212 public double LoadPerTruck(int count, int trucks)
1313 {
14− double totalWeight = count * _unitWeight;
15− return /*^*/totalWeight / trucks;
14+ return TotalWeight(count) / trucks;
15+ }
16+
17+ private double TotalWeight(int count)
18+ {
19+ return count * _unitWeight;
1620 }
1721 }
1822 }

#name-conflict-rejected

Refuses a query name the class already uses for a member

refusal
TargetOrder.cs, the /*^*/ caret
Arguments
name"Quantity"
Refusesname-conflict mentioning “'Quantity'”; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private readonly int[] _lines = { 1, 2, 3 };
6
7 public int Quantity { get; set; }
8
9 public int Total()
10 {
11 int /*^*/count = _lines.Length;
12 return count * 10;
13 }
14 }
15 }

#recipe

Runs the recipe: Extract Method on the initializer, then Inline Local Variable on the temp

successrecipe
Recipe
  1. extract-method on Order.cs, the /*[*/ … /*]*/ selection
    name"BasePrice"
  2. inline-local-variable on symbol M:Shop.Order.Price
    local"basePrice"
Order.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 private readonly int _quantity;
88 private readonly decimal _itemPrice;
99
1010 public Order(int quantity, decimal itemPrice)
1111 {
1212 _quantity = quantity;
1313 _itemPrice = itemPrice;
1414 }
1515
1616 public decimal Price()
1717 {
18− decimal basePrice = /*[*/_quantity * _itemPrice/*]*/;
1918 decimal shipping = Math.Min(_quantity * 2m, 50m);
20− return basePrice + shipping;
19+ return BasePrice() + shipping;
20+ }
21+
22+ private decimal BasePrice()
23+ {
24+ return _quantity * _itemPrice;
2125 }
2226 }
2327 }

#recipe-in-accessor-rejected

The recipe stops at its first step when Extract Method refuses a temp in a property accessor, and nothing changes

refusalrecipe
Recipe
  1. extract-method on Order.cs, the /*[*/ … /*]*/ selection
    name"BasePrice"
  2. inline-local-variable on symbol P:Shop.Order.Price
    local"basePrice"
Refuseserror mentioning “Selected code is not within a method”; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private int _quantity;
6 private decimal _itemPrice;
7
8 public decimal Price
9 {
10 get
11 {
12 decimal basePrice = /*[*/_quantity * _itemPrice/*]*/;
13 return basePrice * 0.98m;
14 }
15 }
16 }
17 }

#several-uses

A temp read in several places, including a condition, becomes a query called at each of them

success
TargetOrder.cs, the /*^*/ caret
Arguments
name"BasePrice"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
55 private readonly int _quantity;
66 private readonly double _itemPrice;
77
88 public Order(int quantity, double itemPrice)
99 {
1010 _quantity = quantity;
1111 _itemPrice = itemPrice;
1212 }
1313
1414 public double Price()
1515 {
16− double /*^*/basePrice = _quantity * _itemPrice;
1716 double discountFactor;
18− if (basePrice > 1000)
17+ if (BasePrice() > 1000)
1918 discountFactor = 0.95;
2019 else
2120 discountFactor = 0.98;
22− return basePrice * discountFactor;
21+ return BasePrice() * discountFactor;
22+ }
23+
24+ private double BasePrice()
25+ {
26+ return _quantity * _itemPrice;
2327 }
2428
2529 public int Quantity()
2630 {
2731 return _quantity;
2832 }
2933 }
3034 }

#side-effects-rejected

Refuses an initializer that calls a method when the temp is read twice, since the call would run at each read

refusal
TargetTicket.cs, the /*^*/ caret
Arguments
name"Next"
Refusesinitializer-has-side-effects; every file is left unchanged
Ticket.csinput
1 namespace Shop
2 {
3 public class Ticket
4 {
5 private int _counter;
6
7 public int Pair()
8 {
9 int /*^*/next = NextNumber();
10 return next + next;
11 }
12
13 private int NextNumber()
14 {
15 return _counter++;
16 }
17 }
18 }

#single-use

Replaces a temp used once with a private query method, as the recipe does

success
TargetOrder.cs, the /*^*/ caret
Arguments
name"BasePrice"
Order.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 private readonly int _quantity;
88 private readonly decimal _itemPrice;
99
1010 public Order(int quantity, decimal itemPrice)
1111 {
1212 _quantity = quantity;
1313 _itemPrice = itemPrice;
1414 }
1515
1616 public decimal Price()
1717 {
18− decimal /*^*/basePrice = _quantity * _itemPrice;
1918 decimal shipping = Math.Min(_quantity * 2m, 50m);
20− return basePrice + shipping;
19+ return BasePrice() + shipping;
20+ }
21+
22+ private decimal BasePrice()
23+ {
24+ return _quantity * _itemPrice;
2125 }
2226 }
2327 }