.NET Tools
Essential productivity kit for .NET and game developers
ReSharper SDK Adventures Part 8 — Expression factoring improved
In the previous part of the SDK Adventures, we started work on a factoring context action for mathematical expressions. We did quite a bit of work to get the factoring algorithm off the ground, but there’s yet more to be done – for example, we need to take care of addition/subtraction and division/multiplication.
But first, we’re going to take a look at testing our plugin.
Testing the Action
With respect to the context action that we’ve been working on, there are two types of test:
-
Availability tests ensure that the action is available where it should be (and, conversely, not available where it shouldn’t).
-
Execution tests ensure that after firing, the context action brings the affected code to its expected state.
In ReSharper’s ecosystem, these tests are data-driven: they all depend on separate ‘before’ and ‘after’ files to be located in specific directories. The tests themselves are typically very predictable in terms of content – all they do is describe the action under test. For example, the execution test for our FactorCA
context action is likely to look as follows:
The above test communicates an expectation that files named execution01.cs
and execution01.cs.gold
can be found in the testdataIntentionsContextActionsFactorCA
folder. This folder is expected to be just above the solution folder, so if the plugin solution is itself in src
, then data
and src
are expected to be in the same folder. Note that the ‘convention over configuration’ approach is pervasive here: ReSharper expected a context action to be in IntentionsContextActions
. You can, of course, customize these locations if you need to.
So let’s look at a simple test: one where we change the expression a*x + a
to a*(x+1)
. We make the original execution01.cs
file with a typical (valid) class declaration having a method, something like
In the above, {caret}
refers to the position of the caret in the file when the action is executed. Our expected result, with the method body omitted, is thus:
Oops! Running the test we see it fail, and if we peek into the .tmp
file in the folder where the test data is stored we see that the generated expression is, in fact, return a*(x +){caret};
– not even valid code. We need to fix something!
Specifically, we need to handle the situation where our histogram of terms doesn’t have a single term to add, in which case we put a 1
in there:
We run the test again, and this time it passes. Great!
Handling Subtraction
As you may remember, we previously noted that IAdditiveExpression
represents both addition and subtraction and that we don’t handle subtraction just yet. Writing a test that fails is very easy:
We first run the test to watch it fail miserably, and then fix the problem. The problem is that when we flatten the initial list of additive expressions, we lose track of whether the expression has a +
or -
sign. To fix this, we introduce the simplest structure possible to retain this information:
Now all usages of IAdditiveExpression
get replaced with CSharpExpressionWithSign
. In fact, we can no longer afford to have a generic Flatten<T>()
method – we need to specialize it:
In the above, IsSubtraction()
is a simple check on the token that’s being examined, i.e.:
Finally, our algorithm for outputting the code needs to be changed to handle negative signs, including negative signs of the very first expression:
And this change is enough to make our latest test pass.
Division
Multiplying by x
can be treated the same as dividing by 1/x
. However, unlike multiplication you cannot ‘factor out’ division at the beginning of the expression. For example, a/x + b/x
is not equal to x/(a+b)
but is rather equal to 1/x*(a+b)
. However, if we do this fully, we also get into the thorny business of reconciling expressions such as x*x / a * x
, which adds an extra layer of complexity and moves us even closer to writing what is effectively a computer algebra system.
For now, rather than significantly increase the complexity, we can simply remove division from our list of candidate variables to factor out. To do that, we check if it’s a multiplication or division in a way similar to IsSubtraction
:
Putting this restriction into the appropriate flattening method we ensure that divisions don’t get included in our term histogram.
Conclusion
We’re slowly taking our plugin into a territory where it’s becoming something of a CAS rather than just an aide to ordinary programming. Beyond a certain point, CAS systems make a lot more sense, so in the next part of SDK adventures, we’ll take a look at an entirely different area that we can enhance.
Meanwhile, check out the source code and stay tuned for Part 9! ■