Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers.Test/OptionListPatternTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier<Funcky.Analyzers.OptionListPatternAnalyzer>;

namespace Funcky.Analyzers.Test;

public sealed class OptionListPatternTest
{
// language=csharp
private const string OptionStub =
"""
namespace Funcky.Monads
{
public readonly struct Option<T>
{
public int Count => throw null!;

public T this[int index] => throw null!;
}
}
""";

[Fact]
public async Task ErrorsWhenListPatternHasMoreThanOneElement()
{
// language=csharp
const string inputCode =
"""
using Funcky.Monads;

class C
{
private void M(Option<string> option)
{
_ = option is ["foo", "bar"];
_ = option is [var foo, var bar, var baz];
_ = option is [var one, var two, var three, var four];
}
}
""";

DiagnosticResult[] expectedDiagnostics = [
VerifyCS.Diagnostic().WithSpan(7, 23, 7, 37),
VerifyCS.Diagnostic().WithSpan(8, 23, 8, 50),
VerifyCS.Diagnostic().WithSpan(9, 23, 9, 62),
];

await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + OptionStub, expectedDiagnostics);
}

[Fact]
public async Task DoesNotErrorWhenUsingAListPatternWithZeroOrOneElements()
{
// language=csharp
const string inputCode =
"""
using Funcky.Monads;

class C
{
private void M(Option<string> option)
{
_ = option is ["foo"];
_ = option is [];
}
}
""";

await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + OptionStub);
}

[Fact]
public async Task UsingASlicePatternIsACompileError()
{
// language=csharp
const string inputCode =
"""
using Funcky.Monads;

class C
{
private void M(Option<string> option)
{
_ = option is [..var slice];
}
}
""";

DiagnosticResult[] expectedDiagnostics = [
DiagnosticResult.CompilerError("CS1503").WithSpan(7, 24, 7, 35).WithArguments("1", "System.Range", "int"),
];

await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + OptionStub, expectedDiagnostics);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
λ1010 | Funcky | Error | OptionListPatternAnalyzer
λ1101 | Funcky | Warning | FunctionalAssertAnalyzer
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AnalyzerRoslynVersion>4.0.1</AnalyzerRoslynVersion>
<DisableAnalyzerReleaseTracking>true</DisableAnalyzerReleaseTracking>
</PropertyGroup>
<Import Project="Funcky.Analyzers.targets" />
</Project>
10 changes: 10 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.targets
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,15 @@
<ItemGroup>
<EmbeddedResource Update="Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
</ItemGroup>
<PropertyGroup Condition="'$(DisableAnalyzerReleaseTracking)' == 'true'">
<!-- RS2008: Enable analyzer release tracking -->
<NoWarn>$(NoWarn);RS2008</NoWarn>
</PropertyGroup>
<Target Name="_FunckyRemoveAnalyzerReleasesFiles" BeforeTargets="CoreCompile" Condition="'$(DisableAnalyzerReleaseTracking)' == 'true'">
<ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
</Target>
<Import Project="..\Funcky.Analyzers.Package\Packing.targets" />
</Project>
49 changes: 49 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers/OptionListPatternAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#if ROSLYN_4_4_0
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Funcky.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class OptionListPatternAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor OptionHasZeroOrOneElements = new(
id: $"{DiagnosticName.Prefix}{DiagnosticName.Usage}10",
title: "An option has either zero or one elements",
messageFormat: "An option has either zero or one elements, testing for more elements will never match",
category: nameof(Funcky),
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(OptionHasZeroOrOneElements);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(OnCompilationStart);
}

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (context.Compilation.GetOptionOfTType() is { } optionOfTType)
{
context.RegisterOperationAction(AnalyzeListPattern(optionOfTType), OperationKind.ListPattern);
}
}

private static Action<OperationAnalysisContext> AnalyzeListPattern(INamedTypeSymbol optionOfTType)
=> context
=>
{
var operation = (IListPatternOperation)context.Operation;
if (SymbolEqualityComparer.Default.Equals(operation.InputType.OriginalDefinition, optionOfTType)
&& operation.Patterns.Length > 1)
{
context.ReportDiagnostic(Diagnostic.Create(OptionHasZeroOrOneElements, operation.Syntax.GetLocation()));
}
};
}
#endif