Skip to content

Commit

Permalink
Support async enumerables
Browse files Browse the repository at this point in the history
  • Loading branch information
aalmada committed Apr 20, 2020
1 parent 2fa9b5e commit cd59205
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 47 deletions.
109 changes: 92 additions & 17 deletions NetFabric.Hyperlinq.Analyzer.UnitTests/NullEnumerableAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,135 @@ protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() =>
new NullEnumerableAnalyzer();

[Fact]
public void Verify()
public void Verify_Enumerable_NoDiagnostics()
{
var test = @"
using System;
using System.Collections.Generic;
class C
{
IEnumerable<int> Method01()
IEnumerable<int> Method_Iterator()
{
return null;
yield return 0;
}
IEnumerable<int> Method02() => null;
IEnumerable<int> Method()
{
return Method_Iterator();
}
IEnumerable<int> MethodArrow() => Method_Iterator();
}";

VerifyCSharpDiagnostic(test);
}

IEnumerable<int> Method03() => new List<int>();
[Fact]
public void Verify_AsyncEnumerable_NoDiagnostics()
{
var test = @"
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class C
{
async IAsyncEnumerable<int> MethodAsync_Iterator()
{
yield return await Task.FromResult(0);
}
IEnumerable<int> Method04()
IAsyncEnumerable<int> MethodAsync()
{
return MethodAsync_Iterator();
}
IAsyncEnumerable<int> MethodArrowAsync() => MethodAsync_Iterator();
}";

VerifyCSharpDiagnostic(test);
}

[Fact]
public void Verify_Enumerable()
{
var test = @"
using System;
using System.Collections.Generic;
class C
{
IEnumerable<int> Method()
{
return null;
yield return 0;
}
IEnumerable<int> MethodArrow() => null;
}";

var method01 = new DiagnosticResult
var expectedMethod = new DiagnosticResult
{
Id = "HLQ002",
Message = "Enumerable cannot be null. Return an empty enumerable instead.",
Severity = DiagnosticSeverity.Error,
Locations = new[] {
new DiagnosticResultLocation("Test0.cs", 9, 9)
},
};

var expectedMethodArrow = new DiagnosticResult
{
Id = "HLQ002",
Message = "Enumerable cannot be null. Return an empty enumerable instead.",
Severity = DiagnosticSeverity.Warning,
Severity = DiagnosticSeverity.Error,
Locations = new[] {
new DiagnosticResultLocation("Test0.cs", 8, 9)
new DiagnosticResultLocation("Test0.cs", 12, 39)
},
};

var method02 = new DiagnosticResult
VerifyCSharpDiagnostic(test, expectedMethod, expectedMethodArrow);
}

[Fact]
public void Verify_AsyncEnumerable()
{
var test = @"
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class C
{
IAsyncEnumerable<int> Method()
{
return null;
}
IAsyncEnumerable<int> MethodArrow() => null;
}";

var expectedMethod = new DiagnosticResult
{
Id = "HLQ002",
Message = "Enumerable cannot be null. Return an empty enumerable instead.",
Severity = DiagnosticSeverity.Warning,
Severity = DiagnosticSeverity.Error,
Locations = new[] {
new DiagnosticResultLocation("Test0.cs", 11, 36)
new DiagnosticResultLocation("Test0.cs", 10, 9)
},
};

var method04 = new DiagnosticResult
var expectedMethodArrow = new DiagnosticResult
{
Id = "HLQ002",
Message = "Enumerable cannot be null. Return an empty enumerable instead.",
Severity = DiagnosticSeverity.Warning,
Severity = DiagnosticSeverity.Error,
Locations = new[] {
new DiagnosticResultLocation("Test0.cs", 17, 9)
new DiagnosticResultLocation("Test0.cs", 13, 44)
},
};

VerifyCSharpDiagnostic(test, method01, method02, method04);
VerifyCSharpDiagnostic(test, expectedMethod, expectedMethodArrow);
}

}
Expand Down
1 change: 1 addition & 0 deletions NetFabric.Hyperlinq.Analyzer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml
Icon.png = Icon.png
LICENSE = LICENSE
NuGet.config = NuGet.config
README.md = README.md
EndProjectSection
EndProject
Expand Down
4 changes: 2 additions & 2 deletions NetFabric.Hyperlinq.Analyzer/NullEnumerableAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public sealed class NullEnumerableAnalyzer : DiagnosticAnalyzer
const string Category = "Performance";

static readonly DiagnosticDescriptor rule =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning,
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error,
isEnabledByDefault: true, description: Description,
helpLinkUri: "https://github.com/NetFabric/NetFabric.Hyperlinq.Analyzer/tree/master/docs/reference/HLQ002_NullEnumerable.md");

Expand All @@ -48,7 +48,7 @@ static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
return;

var returnType = methodSymbol.ReturnType.OriginalDefinition;
if (!returnType.IsEnumerable(context.Compilation, out _))
if (!returnType.IsEnumerable(context.Compilation, out _) && !returnType.IsAsyncEnumerable(context.Compilation, out _))
return;

var arrowExpressionClauseSyntax = methodDeclarationSyntax.DescendantNodes()
Expand Down
15 changes: 15 additions & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageRestore>
<!-- Allow NuGet to download missing packages -->
<add key="enabled" value="True" />

<!-- Automatically check for missing packages during build in Visual Studio -->
<add key="automatic" value="True" />
</packageRestore>
<packageSources>
<clear /> <!-- ensure only the sources defined below are used -->
<add key="local" value="LocalNuget" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@

# NetFabric.Hyperlinq.Analyzer

A Roslyn Analyzer that contains several enumeration-related rules to help users improve performance.
A [Roslyn Analyzer](https://docs.microsoft.com/en-us/visualstudio/code-quality/roslyn-analyzers-overview) that contains several rules to help improve enumeration performance when using C#.

The analyzer is independent of `NetFabric.Hyperlinq`. The rules are useful even if you only use `IEnumerable<T>` or `System.Linq`.
Check the documentation for the implemented rules at https://github.com/NetFabric/NetFabric.Hyperlinq.Analyzer/tree/master/docs/reference.

Check the documentation for the implemented rules at https://github.com/NetFabric/NetFabric.Hyperlinq.Analyzer/tree/master/docs/reference
**Note:** This analyzer is independent of [`NetFabric.Hyperlinq`](https://github.com/NetFabric/NetFabric.Hyperlinq). The rules may be useful when you only use `foreach`, `IEnumerable<T>`, `IAsyncEnumerable<T>`, `System.Linq` or `System.Linq.Async`.

# Usage

Add the [NetFabric.Hyperlinq.Analyzer](https://www.nuget.org/packages/NetFabric.Hyperlinq.Analyzer/) package to your project using your favorite NuGet client.

If added manually to the `.csproj`, make sure to set `PrivateAssets` to `all` so that it's not added as a dependency. A [floating version](https://docs.microsoft.com/en-us/nuget/concepts/dependency-resolution#floating-versions) can be used to get the latest version.

``` xml
<PackageReference Include="NetFabric.Hyperlinq.Analyzer" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
```

# References

Expand Down
4 changes: 4 additions & 0 deletions docs/reference/HLQ000_Template.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Cause

## Severity

Error/Warning

## Rule description

## How to fix violations
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/HLQ001_AssignmentBoxing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

An enumerable that creates value type enumerators is assigned to a non-public variable, field or property whose type causes the enumerator to be boxed.

## Severity

Warning

## Rule description

Enumerables can be implemented so that they create value type enumerators. The advantage is that calls to its methods are not virtual.
Expand Down
74 changes: 49 additions & 25 deletions docs/reference/HLQ002_NullEnumerable.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

A method that returns an enumerable type is returning `null`.

## Rule description
## Severity

Error

Returning `null`, in the context of enumerables, is not the same as an empty enumerable. It's an invalid state.
## Rule description

The following `foreach` loop:

Expand Down Expand Up @@ -38,27 +40,19 @@ finally
}
```

Notice that, because `DoSomething()` returns `null`, [a `NullReferenceException` will be thrown](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAIAYAEqCMBuAWACgNscAWQo41AZjIDZsAmMgdk2IG9jM/t6uJqnKYAsgEMAllAAUASk5F+mHspX8AZgHswAUwkBjABaZZANwlhMU4HoC2NqJgDie4AFEoEe3rASAIwAbPQV5Xg1I3ABOWVsHeSoVAF8IvjTGMloAHhlgAD5Xdy8fP0CQhUwAXkLvIKCqZKA==) when calling `GetEnumerator()`.
Because `DoSomething()` returns `null`, [a `NullReferenceException` will be thrown](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAIAYAEqCMBuAWACgNscAWQo41AZjIDZsAmMgdk2IG9jM/t6uJqnKYAsgEMAllAAUASk5F+mHspX8AZgHswAUwkBjABaZZANwlhMU4HoC2NqJgDie4AFEoEe3rASAIwAbPQV5Xg1I3ABOWVsHeSoVAF8IvjTGMloAHhlgAD5Xdy8fP0CQhUwAXkLvIKCqZKA==) when trying to call `GetEnumerator()`.

The enumerable should instead make the enumerator `MoveNext()` return `false` to stop the enumeration loop.
The same issue applies to `IAsyncEnumerable<T>`.

## How to fix violations
`null` is not equivalent to an empty enumerable. An empty collection, is a collection where calls to its `MoveNext()` returns always `false`. While `null` is an invalid state.

You can return an empty instance of an array or `List<T>`, or one of the following implementations of an empty enumerable:

[`System.Linq.Enumerable.Empty<TResult>()`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.empty), if method returns any of the following:
- `IEnumerable`
- `IEnumerable<TResult>`
## How to fix violations

`NetFabric.Hyperlinq.Enumerable.Empty<TResult>()`, if method returns any of the following:
- `IEnumerable`
- `IEnumerable<TResult>`
- `IReadOnlyCollection<TResult>`
- `IReadOnlyList<TResult>`
Return an instance of an empty collection.

## When to suppress warnings

Supress warning only if you really don't want to return an empty enumerable.
Should not be suppressed.

## Example of a violation

Expand All @@ -67,9 +61,19 @@ Supress warning only if you really don't want to return an empty enumerable.
### Code

```csharp
IEnumerable<int> Method(bool condition)
IEnumerable<int> Method()
{
if (...)
return null;

...
}
```

```csharp
IAsyncEnumerable<int> Method()
{
if (condition)
if (...)
return null;

...
Expand All @@ -81,10 +85,12 @@ IEnumerable<int> Method(bool condition)
Using an empty array:

```csharp
IEnumerable<int> Method1(bool condition)
using System;

IEnumerable<int> Method()
{
if (condition)
return new int[0];
if (...)
return Array.Empty<int>();

...
}
Expand All @@ -93,9 +99,11 @@ IEnumerable<int> Method1(bool condition)
Using an empty `List<T>`:

```csharp
IEnumerable<int> Method1(bool condition)
using System.Collections.Generic;

IEnumerable<int> Method()
{
if (condition)
if (...)
return new List<int>();

...
Expand All @@ -105,12 +113,28 @@ IEnumerable<int> Method1(bool condition)
Using `Enumerable.Empty<int>()`:

```csharp
IEnumerable<int> Method1(bool condition)
using System.Linq;

IEnumerable<int> Method()
{
if (condition)
if (...)
return Enumerable.Empty<int>();

...
}
```

Using `AsyncEnumerable.Empty<int>()` (requires [System.Linq.Async](https://www.nuget.org/packages/System.Linq.Async/) package):

```csharp
using System.Linq.Async;

IAsyncEnumerable<int> Method()
{
if (...)
return AsyncEnumerable.Empty<int>();

...
}
```

4 changes: 4 additions & 0 deletions docs/reference/HLQ003_HighestLevelInterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

Public method returns a lower level interface than the one supported by the value returned.

## Severity

Warning

## Rule description

`IEnumerable<T>` is commonly used to return a read-only view of a collection. This interface only allows sequencial enumeration of the items, resulting in methods like [`Count()`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.count) to have complexity O(n).
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/HLQ004_RefEnumerationVariable.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

The enumerator returns a reference to the items. The enumeration variable is not declared as a reference so a copy of each item will be made.

## Severity

Warning

## Rule description

## How to fix violations
Expand Down
Loading

0 comments on commit cd59205

Please sign in to comment.