📜 ⬆️ ⬇️

ReSharper: Value Tracking

I already wrote about the new feature of the 5th Resharper Call Hierarchy . The logical development of Call Hierarchy is Value Tracking. Value Tracking was created to help the developer understand how incorrect data could come to a specific point in the program, or where this data could go. As a result, it becomes easier to investigate the causes of a NullReferenceException or incorrect behavior and output.

Again, I will not deeply theorize, but I’ll show you how and in what scenarios Value Tracking works.

Simple example


Consider a simple example. In the VisitData method, a VisitData occurs, let's find out where null comes from. Place the caret to use the dc parameter in the VisitData method and run the analysis ( R # -> Inspect -> Value Origin ):

public class Main
{
void Start()
{
Console .WriteLine(Do( new DataClass()));
}

void Start2()
{
Console .WriteLine(Do( null ));
}

int Do(DataClass data)
{
var v = new Visitor();
return v.VisitData(data);
}
}

public class DataClass
{
public int GetData()
{
return 0;
}
}

public class Visitor
{
public int VisitData(DataClass dc)
{
return dc.GetData();
}
}


* This source code was highlighted with Source Code Highlighter .

')
image

If you start the analysis on the example above and navigate through the results tree, you will find that the tree contains all the nodes that are interesting for the programmer:
  1. Directly use dc (just the place where the exception occurs)
  2. Passing the data parameter to the VisitData method
  3. Calling the Do method with “good data” (in the tree, the data of interest to us is highlighted by the bolt )
  4. Calling the Do method with null is the problem we are looking for.
By and large, nothing but massive Find Usages was done. But ValueTracking:
Value Tracking is especially convenient if variable names are constantly changing, data is added to a collection, and transmitted through closures. Let's move on to these more complex and interesting cases.

Inheritance


This time we have an interface, its implementations, fields, field initializers, constructors. Let's try to figure out what values ​​the Main.Start method can display. To do this, select the expression dataProvider.Foo and call Value Origin on it:

public interface IInterface
{
int Foo();
}

public class Base1 : IInterface
{
public virtual int Foo()
{
return 1;
}
}

public class Base2 : IInterface
{
private readonly int _foo = 2;

public Base2()
{
}

public Base2( int foo)
{
this ._foo = foo;
}

public virtual int Foo()
{
return _foo;
}
}

public class Main
{
public void Start(IInterface dataProvider)
{
Console .WriteLine(dataProvider.Foo());
}

public void Usage()
{
Start( new Base2(3));
}
}


* This source code was highlighted with Source Code Highlighter .


image

In the results of Value Tracking, we see:
  1. An implementation of the Foo method, which returns a constant of 1
  2. The implementation of the Foo method, which returns the value of the _foo field, as well as all sources of values ​​for this field:
    1. Assigning a value to this field in the constructor
    2. Calling constructor with parameter 3
    3. Initializer of this field with a value of 2
Those. we literally in a few steps found all possible values. Imagine now how much time you will save if you have spreading hierarchies and complex logic?

Collections


Now consider the work with collections. Let's try to figure out the set of all values ​​that will be displayed on the screen with the following code. To do this, let’s use i inside Console.WriteLine and run the Value Origin analysis:

class Main
{
void Foo()
{
var list = new List < int >();
list.AddRange(GetData());
list.AddRange(GetDataLazy());
ModifyData(list);

foreach ( var i in list)
{
Console .WriteLine(i);
}
}

void ModifyData( List < int > list)
{
list.Add(6);
}

private IEnumerable < int > GetData()
{
return new [] { 1, 2, 3 };
}

IEnumerable < int > GetDataLazy()
{
yield return 4;
yield return 5;
}
}


* This source code was highlighted with Source Code Highlighter .


image

We found both explicit array creation, values ​​that come from a lazy enumerator, and even a call to the Add method. Sumptuously!

Collections in the opposite direction, or where the values ​​go


And now we will try in the opposite direction, let's see where the number 5 will go. Select it and call Value Destination:

public class testMy
{
void Do()
{
int x = 5;
var list = Foo(x);

foreach ( var item in list)
{
Console .WriteLine(item);
}
}

List < int > Foo( int i)
{
var list = new List < int >();
list.Add(i);
return list;
}
}


* This source code was highlighted with Source Code Highlighter .


image

Quickly enough we found out that number 5 :
  1. Transferred to the Foo method
  2. Added to collection
  3. The collection is returned and used.
  4. Collection items displayed on screen.

In this and previous examples, note that as soon as Value Tracking moves from value tracking to collection tracking, the corresponding nodes in the tree are marked with a special pink icon.

Lambda


Lambdas, with them, especially if there are many of them and do not let them fight, they are constantly having problems. Let's see how R # works in the following situation. At the same time, let's try to trace let values ​​in both directions:

public class MyClass
{
void Main()
{
var checkFunction = GetCheckFunction();
Console .WriteLine(checkFunction(1));
}

Func< int , bool > GetCheckFunction()
{
Func< int , bool > myLambda = x =>
{
Console .WriteLine(x);
return x > 100; //
};
return myLambda;
}
}


* This source code was highlighted with Source Code Highlighter .

First we will look for where the values ​​of the parameter x are taken. Select its use in the Console.WriteLine call and call Value Origin:

image

  1. Found containing lambda parameter
  2. Further analysis traced where this lambda is transmitted. Please note that all nodes in which we track the lambda are marked with a special icon.
  3. In the last step, we see that lambda is called with argument 1 , this is the desired value for x

Now let's try to find where the value returned by lambda is used. Select x>100 , and call Value Destination ( R # -> Inspect -> Value Destination ):

image
  1. Analysis keeps track of what is returned as the result of the lambda.
  2. Then R # traced where the lambda was transmitted
  3. In the end, we see a call to the WriteLine method, which uses the value returned by the lambda.

You can easily make a more complex example with nested lambdas yourself by replacing the output to the screen ( Console.WriteLine ) with two lines:

Func<Func< int , bool >, int , bool > invocator = (func, i) => func(i);
Console .WriteLine(invocator (checkFunction,1));


* This source code was highlighted with Source Code Highlighter .

The analysis will continue to work and you can easily find out where the value of the expression x>100 falls. The code with nested lambdas is very difficult for an ordinary person to understand, which makes the analysis even more popular. Moreover, you can try to create a collection of nested lambdas - and it will work! But such exercises I will leave the reader and the difficult real life.

Source: https://habr.com/ru/post/85693/


All Articles