Skip to content

Commit

Permalink
feat!: explicit record class declarations (#94)
Browse files Browse the repository at this point in the history
add analyzer for reference records without class keyword
add fixer adding optional class keyword to records that are reference types
add support for Visual Studio 2022 to the VSIX
add link to NuGet Package Explorer to README
fix null-check analyzer reporting on static bool Equals(object) methods
update Roslyn from 3.8 to 4.0

BREAKING CHANGE: drop support for .NET 5.0 SDK and Visual Studio 2019
  • Loading branch information
Flash0ver authored Feb 3, 2022
1 parent 0692732 commit acf5fe7
Show file tree
Hide file tree
Showing 35 changed files with 765 additions and 193 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
CHANGELOG

## vNext
### Analyzers
- Added `ImplicitRecordClassDeclaration` diagnostic analyzer, reporting _Warning F02001_ for `record` declarations without the `class` or `struct` keyword.
- Added `DeclareRecordClassExplicitly` code fix provider, adding the optional `class` keyword to `record` declarations that are _reference types_.
- Fixed `PreferPatternMatchingNullCheckOverComparisonWithNull` diagnostic analyzer, throwing `System.NullReferenceException` on invocations of `bool`-returning `static` methods named `Equals` with a single `null` argument.

### NuGet package
- Added support for `.NET SDK 6.0`.
- Removed support for `.NET SDK 5.0`.

### Visual Studio Extension
- Added support for `Visual Studio 2022`.
- Removed support for `Visual Studio 2017` and `Visual Studio 2019`.

## v0.8.0 (2021-10-13)
### Analyzers
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Code refactorings and diagnostic analyzers with code fixes for C# projects, base
[Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=Flash0Ware.F0-Analyzers-VS)

## API Browser / Package Viewer
[fuget](https://www.fuget.org/packages/F0.Analyzers)\
[NuGet Package Explorer](https://nuget.info/packages/F0.Analyzers)\
[FuGet Package Explorer](https://www.fuget.org/packages/F0.Analyzers)\
[DotNetApis](http://dotnetapis.com/pkg/F0.Analyzers)\
[NuGet Must Haves](https://nugetmusthaves.com/Package/F0.Analyzers)\
[NuGet Trends](https://nugettrends.com/packages?months=12&ids=F0.Analyzers)
Expand All @@ -31,12 +32,12 @@ See [CHANGELOG.md](./CHANGELOG.md).
See [CONTRIBUTING.md](./CONTRIBUTING.md).

## Compatibility
requires _Roslyn v3.8.0_ or later (see [packages of the .NET Compiler Platform](https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md))
### .NET 5.0
supports [SDK 5.0.100](https://github.com/dotnet/core/blob/main/release-notes/5.0/5.0.0/5.0.0.md) and later
### Visual Studio 2019
supports [16.8.0](https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.8) and later
requires _Roslyn v4.0.1_ or later (see [packages of the .NET Compiler Platform](https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md))
### .NET 6.0
supports [SDK 6.0.100](https://github.com/dotnet/core/blob/main/release-notes/6.0/6.0.0/6.0.0.md) and later
### Visual Studio 2022
supports [17.0.0](https://docs.microsoft.com/en-us/visualstudio/releases/2022/release-notes#17.0.0) and later
### Rider
supports [2020.3](https://www.jetbrains.com/rider/whatsnew/2020-3/) and later
supports [2021.3](https://www.jetbrains.com/rider/whatsnew/2021-3/) and later
### Visual Studio Code
supports _OmniSharp (C#)_ [1.23.7](https://github.com/OmniSharp/omnisharp-vscode/releases/tag/v1.23.7) and later
supports _OmniSharp (C#)_ [1.24.0](https://github.com/OmniSharp/omnisharp-vscode/releases/tag/v1.24.0) and later
3 changes: 3 additions & 0 deletions documentation/F0.Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ Namespace: [F0.CodeAnalysis.Diagnostics](../source/production/F0.Analyzers/CodeA
* F00001 [GoToStatementConsideredHarmful](./diagnostics/F00001.md)
* BestPractice
* F0100x [PreferPatternMatchingNullCheckOverComparisonWithNull](./diagnostics/F0100x.md)
* CleanCode
* F02001 [ImplicitRecordClassDeclaration](./diagnostics/F02001.md)

## Code Fixes

Namespace: [F0.CodeAnalysis.CodeFixes](../source/production/F0.Analyzers/CodeAnalysis/CodeFixes/)

* [UsePatternMatchingNullCheckInsteadOfComparisonWithNull](./fixes/UsePatternMatchingNullCheckInsteadOfComparisonWithNull.md)
* [DeclareRecordClassExplicitly](./fixes/DeclareRecordClassExplicitly.md)

## Code Refactorings

Expand Down
39 changes: 39 additions & 0 deletions documentation/diagnostics/F02001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Avoid implicit reference type records

DiagnosticAnalyzer: [F02001ImplicitRecordClassDeclaration.cs](../../source/production/F0.Analyzers/CodeAnalysis/Diagnostics/F02001ImplicitRecordClassDeclaration.cs)

| | |
|------------|--------------------|
| ID | F02001 |
| Category | CleanCode |
| Language | C# 10.0 or greater |
| Applies to | `[vNext,)` |

## Summary

Clarify that a `record` is a _reference type_ with the `record class` declaration.

## Remarks

_Record types_, introduced in _C# 9.0_, define _reference types_ with _value equality_.
In _C# 10_ and later, you can explicitly declare a `record class`, which is semantically identical to a `record`.

## Example

```cs
public record Record(int Number, string Text); // F02001
public record class RecordClass(int Number, string Text);
public record struct RecordStruct(int Number, string Text);
public readonly record struct ReadonlyRecordStruct(int Number, string Text);
```

## See also

- [What's new in C# 9.0](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9)
- [What's new in C# 10](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-10)
- [Records](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record)
- [Reference types](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types)

## History

- [vNext](../../CHANGELOG.md#vNext)
42 changes: 42 additions & 0 deletions documentation/fixes/DeclareRecordClassExplicitly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Prefer explicit reference type records

CodeFixProvider: [DeclareRecordClassExplicitly.cs](../../source/production/F0.Analyzers/CodeAnalysis/CodeFixes/DeclareRecordClassExplicitly.cs)

| | |
|------------|--------------------------------------------------------------|
| Title | Explicitly add class keyword to reference record declaration |
| Fixes | [F02001][F02001] |
| Language | C# 10.0 or greater |
| Applies to | `[vNext,)` |

## Summary

Adds the optional `class` keyword to _reference record_ declarations to add clarity for readers.

## Remarks

Both a `record` and a `record class` declare a _reference type_ and are semantically equal.
The `class` keyword is optional, distinguishing these types from `record struct` and `readonly record struct` declarations, both value types with the semantic difference of immutability.

## Example

Before:
```cs
public record Record(int Number, string Text);
```

After:
```cs
public record class Record(int Number, string Text);
```

## See also

- [What's new in C# 10 - Record structs](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-10#record-structs)

## History

- [vNext](../../CHANGELOG.md#vNext)


[F02001]: ../diagnostics/F02001.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Use Pattern Matching to check for null

CodeRefactoringProvider: [UsePatternMatchingNullCheckInsteadOfComparisonWithNull.cs](../../source/production/F0.Analyzers/CodeAnalysis/CodeFixes/UsePatternMatchingNullCheckInsteadOfComparisonWithNull.cs)
CodeFixProvider: [UsePatternMatchingNullCheckInsteadOfComparisonWithNull.cs](../../source/production/F0.Analyzers/CodeAnalysis/CodeFixes/UsePatternMatchingNullCheckInsteadOfComparisonWithNull.cs)

| | |
|------------|------------------------------------|
Expand Down
4 changes: 4 additions & 0 deletions source/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Project>

<PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' Or ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '3.0'))) Or ('$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '2.1')))">
<NoWarn>$(NoWarn);nullable</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net5.0'))">
<DefineConstants>$(DefineConstants);HAS_REFERENCE_EQUALITY_COMPARER</DefineConstants>
</PropertyGroup>
Expand Down
3 changes: 3 additions & 0 deletions source/F0.Analyzers.Core.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
"solution": {
"path": "F0.Analyzers.sln",
"projects": [
"example\\F0.Analyzers.Example.CSharp10\\F0.Analyzers.Example.CSharp10.csproj",
"example\\F0.Analyzers.Example.CSharp2\\F0.Analyzers.Example.CSharp2.csproj",
"example\\F0.Analyzers.Example.CSharp7\\F0.Analyzers.Example.CSharp7.csproj",
"example\\F0.Analyzers.Example.CSharp8\\F0.Analyzers.Example.CSharp8.csproj",
"example\\F0.Analyzers.Example.CSharp9\\F0.Analyzers.Example.CSharp9.csproj",
"example\\F0.Analyzers.Example.Dependencies\\F0.Analyzers.Example.Dependencies.csproj",
"example\\F0.Analyzers.Example\\F0.Analyzers.Example.csproj",
"package\\F0.Analyzers.Package\\F0.Analyzers.Package.csproj",
Expand Down
1 change: 1 addition & 0 deletions source/F0.Analyzers.Example.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"solution": {
"path": "F0.Analyzers.sln",
"projects": [
"example\\F0.Analyzers.Example.CSharp10\\F0.Analyzers.Example.CSharp10.csproj",
"example\\F0.Analyzers.Example.CSharp2\\F0.Analyzers.Example.CSharp2.csproj",
"example\\F0.Analyzers.Example.CSharp7\\F0.Analyzers.Example.CSharp7.csproj",
"example\\F0.Analyzers.Example.CSharp8\\F0.Analyzers.Example.CSharp8.csproj",
Expand Down
7 changes: 7 additions & 0 deletions source/F0.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "F0.Analyzers.Example.CSharp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "F0.Analyzers.Example.CSharp9", "example\F0.Analyzers.Example.CSharp9\F0.Analyzers.Example.CSharp9.csproj", "{BF6085B7-EF86-49A1-9FED-9308982A7A89}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F0.Analyzers.Example.CSharp10", "example\F0.Analyzers.Example.CSharp10\F0.Analyzers.Example.CSharp10.csproj", "{4FF74BB6-FD3D-443A-99F7-A5113EBE56C4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -99,6 +101,10 @@ Global
{BF6085B7-EF86-49A1-9FED-9308982A7A89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF6085B7-EF86-49A1-9FED-9308982A7A89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF6085B7-EF86-49A1-9FED-9308982A7A89}.Release|Any CPU.Build.0 = Release|Any CPU
{4FF74BB6-FD3D-443A-99F7-A5113EBE56C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FF74BB6-FD3D-443A-99F7-A5113EBE56C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FF74BB6-FD3D-443A-99F7-A5113EBE56C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FF74BB6-FD3D-443A-99F7-A5113EBE56C4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -117,6 +123,7 @@ Global
{8843371B-AB4A-4262-B027-0ADE86F74E71} = {085A4A4F-05CF-4844-B41D-6C85391E6869}
{0C22B8EE-947D-4962-BBB3-DC1381E96436} = {EF42DC6B-A41F-4459-83E0-296E02BD86B9}
{BF6085B7-EF86-49A1-9FED-9308982A7A89} = {EF42DC6B-A41F-4459-83E0-296E02BD86B9}
{4FF74BB6-FD3D-443A-99F7-A5113EBE56C4} = {EF42DC6B-A41F-4459-83E0-296E02BD86B9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4C250660-2192-45B9-A87B-5E23C0AAA835}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace F0.Analyzers.Example.CSharp10.CodeAnalysis.CodeFixes
{
internal sealed class DeclareRecordClassExplicitlyExamples
{
public record Record(int Number, string Text);

public record class RecordClass(int Number, string Text);

public record struct RecordStruct(int Number, string Text);

public readonly record struct ReadonlyRecordStruct(int Number, string Text);

public class Class { }

public static class StaticClass { }

public struct Struct { }

public readonly struct ReadonlyStruct { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="16.8.3038" PrivateAssets="all" />
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="17.0.5234" PrivateAssets="all" />
</ItemGroup>

<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
<Preview>false</Preview>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[15.0,17.0)" />
<InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[17.0,18.0)">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
</Installation>
<Assets>
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="F0.Analyzers" Path="|F0.Analyzers|"/>
Expand All @@ -24,7 +26,7 @@
<Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.5,)" />
</Dependencies>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[15.0,)" DisplayName="Visual Studio core editor" />
<Prerequisite Id="Microsoft.VisualStudio.Component.Roslyn.LanguageServices" Version="[15.0,)" DisplayName="Roslyn Language Services" />
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,)" DisplayName="Visual Studio core editor" />
<Prerequisite Id="Microsoft.VisualStudio.Component.Roslyn.LanguageServices" Version="[17.0,)" DisplayName="Roslyn Language Services" />
</Prerequisites>
</PackageManifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using F0.Benchmarking.CodeAnalysis;
using F0.Benchmarking.CodeAnalysis.CodeFixes;
using F0.CodeAnalysis.CodeFixes;
using F0.CodeAnalysis.Diagnostics;

namespace F0.Benchmarks.CodeAnalysis.CodeFixes;

public class DeclareRecordClassExplicitlyBenchmarks
{
private readonly CodeFixBenchmark<F02001ImplicitRecordClassDeclaration, DeclareRecordClassExplicitly> benchmark;

public DeclareRecordClassExplicitlyBenchmarks()
{
benchmark = Measure.CodeFix<F02001ImplicitRecordClassDeclaration, DeclareRecordClassExplicitly>();
}

[GlobalSetup]
public void Setup()
{
var code =
@"using System;
record Record;
";

benchmark.Initialize(code, LanguageVersion.Latest);
}

[Benchmark]
public Task UseConstantNullPattern()
=> benchmark.InvokeAsync();

[GlobalCleanup]
public void Cleanup()
{
var code =
@"using System;
record class Record;
";

benchmark.Inspect(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using F0.Benchmarking.CodeAnalysis;
using F0.Benchmarking.CodeAnalysis.Diagnostics;
using F0.CodeAnalysis.Diagnostics;

namespace F0.Benchmarks.CodeAnalysis.Diagnostics;

public class F02001ImplicitRecordClassDeclarationBenchmarks
{
private readonly DiagnosticAnalyzerBenchmark<F02001ImplicitRecordClassDeclaration> benchmark;

public F02001ImplicitRecordClassDeclarationBenchmarks()
{
benchmark = Measure.DiagnosticAnalyzer<F02001ImplicitRecordClassDeclaration>();
}

[GlobalSetup]
public void Setup()
{
var code =
@"using System;
record Record(int Number, string Text);
record class RecordClass(int Number, string Text);
record struct RecordStruct(int Number, string Text);
readonly record struct ReadonlyRecordStruct(int Number, string Text);
[Obsolete]
sealed record @record<T> : IDisposable where T : notnull
{
public T Property { get; init; }
public void Dispose() => throw new NotImplementedException();
}
";

benchmark.Initialize(code, LanguageVersion.Latest);
}

[Benchmark]
public Task GoToStatementConsideredHarmful()
=> benchmark.InvokeAsync();

[GlobalCleanup]
public void Cleanup()
{
var diagnostics = benchmark.CreateDiagnostics(
d => d.WithLocation(3, 8, 3, 14).WithArguments("Record"),
d => d.WithLocation(9, 15, 9, 22).WithArguments("record")
);

benchmark.Inspect(diagnostics);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit acf5fe7

Please sign in to comment.