Analyzer rules

edit

The package ships Roslyn analyzers and code fixes. They run in the IDE and during the build as soon as the package is referenced: nothing else to install. They report two kinds of problems:

  • assertions that cannot work, reported as errors: an assertion that is never awaited, that always passes, or that compares a value type by reference;
  • assertions that could be clearer, reported as warnings: a generic Assert.True(...) where a specialized assertion would write a better failure message, or arguments in the wrong order.

Most rules come with a code fix: apply it from the light bulb of your IDE, or fix all the occurrences of a rule at once.

Assertions that cannot work

IdReports
MFAS0048An assertion that returns a Task, such as an assertion on an IAsyncEnumerable<T> or Throws with an asynchronous delegate, that is not awaited. It never runs
MFAS0057A delegate that returns an awaitable the assertion does not await, such as a ValueTask<T>. Wrap the call in an async lambda
MFAS0049An assertion that always produces the same result, such as Assert.True(true) or a value compared with itself
MFAS0002Assert.ReferenceEquals instead of Assert.Same
MFAS0003Assert.IsType with a static or abstract type: no value is exactly of such a type
MFAS0008, MFAS0009Assert.Null or Assert.NotNull with a non-nullable value type, which is never null
MFAS0010, MFAS0011Assert.Same or Assert.NotSame with a value type, which is boxed into a new object on each call

Argument order

MFAS0001 reports a call whose actual value is a constant while its expected value is not, such as Assert.Equal(result, 42). The failure message would label the values the wrong way round. The code fix swaps them: Assert.Equal(42, result).

Specialized assertions

A specialized assertion writes a better failure message than Assert.True or Assert.Equal on a derived value: it shows the collection, the text or the range, not only false.

Instead ofWriteId
Assert.True(text.Contains("x"))Assert.Contains("x", text)MFAS0016
Assert.True(text.StartsWith("x"))Assert.StartsWith("x", text)MFAS0018
Assert.True(text.EndsWith("x"))Assert.EndsWith("x", text)MFAS0020
Assert.True(Regex.IsMatch(text, pattern))Assert.Matches(pattern, text)MFAS0014
Assert.True(list.Contains(item))Assert.Contains(item, list)MFAS0022
Assert.True(list.Any(x => x > 0))Assert.Contains(list, x => x > 0)MFAS0024
Assert.True(list.All(x => x > 0))Assert.All(list, x => x > 0)MFAS0026
Assert.True(list.Any())Assert.NotEmpty(list)MFAS0028
Assert.NotEmpty(list.Where(x => x > 0))Assert.Contains(list, x => x > 0)MFAS0051
Assert.Single(list.Where(x => x > 0))Assert.Single(list, x => x > 0)MFAS0053
Assert.Contains(list, x => x == 42)Assert.Contains(42, list)MFAS0054
Assert.Equal(3, list.Count)Assert.HasCount(3, list)MFAS0005
Assert.Equal(0, list.Count), Assert.True(list.Count > 0)Assert.Empty(list), Assert.NotEmpty(list)MFAS0004
Assert.HasCount(0, list)Assert.Empty(list)MFAS0030
Assert.True(value == 42)Assert.Equal(42, value)MFAS0031
Assert.True(value.Equals(other))Assert.Equal(value, other)MFAS0033
Assert.True(actual.SequenceEqual(expected))Assert.Equal(expected, actual)MFAS0035
Assert.True(ReferenceEquals(a, b))Assert.Same(a, b)MFAS0037
Assert.True(value == null)Assert.Null(value)MFAS0006
Assert.True(value is string)Assert.IsAssignableTo<string>(value)MFAS0012
Assert.True(value.GetType() == typeof(List<int>))Assert.IsType<List<int>>(value)MFAS0046
Assert.True(0 <= value && value <= 10)Assert.InRange(value, 0, 10)MFAS0039
Assert.True(set.IsProperSubsetOf(other))Assert.ProperSubset(other, set)MFAS0041
Assert.True(!condition)Assert.False(condition)MFAS0045
Assert.True(false)Assert.Fail()MFAS0050

Each rule that rewrites Assert.True(...) has a twin for the negated form, Assert.False(...), which suggests the negative assertion: DoesNotContain, DoesNotStartWith, DoesNotEndWith, DoesNotMatch, DoesNotAll, Empty, NotEqual, NotSame, NotNull, IsNotAssignableTo, IsNotType, NotInRange, NotProperSubset or NotProperSuperset (MFAS0007, MFAS0013, MFAS0015, MFAS0017, MFAS0019, MFAS0021, MFAS0023, MFAS0025, MFAS0027, MFAS0029, MFAS0032, MFAS0034, MFAS0036, MFAS0038, MFAS0040, MFAS0042, MFAS0044, MFAS0047). The same goes for Assert.Empty(list.Where(...)) (MFAS0052), Assert.DoesNotContain(list, x => x == 42) (MFAS0055), and Assert.True(set.IsProperSupersetOf(other)) (MFAS0043).

Use the returned value

MFAS0056, reported as a suggestion, points to code that derives again a value the assertion returned, such as collection[0] or collection.First() after Assert.Single(collection). Its code fix stores the returned value in a variable and uses it. See Getting started.

Change the severity of a rule

Set the severity of a rule in the .editorconfig file of your test projects, like any other analyzer rule:

[*.cs]
dotnet_diagnostic.MFAS0005.severity = suggestion
dotnet_diagnostic.MFAS0056.severity = none

The severity can be error, warning, suggestion, silent or none.