Skip to content

Commit 6f17806

Browse files
authored
Merge pull request #850 from polyadic/list-pattern-analyzer
2 parents 8e3eab4 + b1b6714 commit 6f17806

File tree

5 files changed

+156
-0
lines changed

5 files changed

+156
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using Microsoft.CodeAnalysis.Testing;
2+
using Xunit;
3+
using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier<Funcky.Analyzers.OptionListPatternAnalyzer>;
4+
5+
namespace Funcky.Analyzers.Test;
6+
7+
public sealed class OptionListPatternTest
8+
{
9+
// language=csharp
10+
private const string OptionStub =
11+
"""
12+
namespace Funcky.Monads
13+
{
14+
public readonly struct Option<T>
15+
{
16+
public int Count => throw null!;
17+
18+
public T this[int index] => throw null!;
19+
}
20+
}
21+
""";
22+
23+
[Fact]
24+
public async Task ErrorsWhenListPatternHasMoreThanOneElement()
25+
{
26+
// language=csharp
27+
const string inputCode =
28+
"""
29+
using Funcky.Monads;
30+
31+
class C
32+
{
33+
private void M(Option<string> option)
34+
{
35+
_ = option is ["foo", "bar"];
36+
_ = option is [var foo, var bar, var baz];
37+
_ = option is [var one, var two, var three, var four];
38+
}
39+
}
40+
""";
41+
42+
DiagnosticResult[] expectedDiagnostics = [
43+
VerifyCS.Diagnostic().WithSpan(7, 23, 7, 37),
44+
VerifyCS.Diagnostic().WithSpan(8, 23, 8, 50),
45+
VerifyCS.Diagnostic().WithSpan(9, 23, 9, 62),
46+
];
47+
48+
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + OptionStub, expectedDiagnostics);
49+
}
50+
51+
[Fact]
52+
public async Task DoesNotErrorWhenUsingAListPatternWithZeroOrOneElements()
53+
{
54+
// language=csharp
55+
const string inputCode =
56+
"""
57+
using Funcky.Monads;
58+
59+
class C
60+
{
61+
private void M(Option<string> option)
62+
{
63+
_ = option is ["foo"];
64+
_ = option is [];
65+
}
66+
}
67+
""";
68+
69+
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + OptionStub);
70+
}
71+
72+
[Fact]
73+
public async Task UsingASlicePatternIsACompileError()
74+
{
75+
// language=csharp
76+
const string inputCode =
77+
"""
78+
using Funcky.Monads;
79+
80+
class C
81+
{
82+
private void M(Option<string> option)
83+
{
84+
_ = option is [..var slice];
85+
}
86+
}
87+
""";
88+
89+
DiagnosticResult[] expectedDiagnostics = [
90+
DiagnosticResult.CompilerError("CS1503").WithSpan(7, 24, 7, 35).WithArguments("1", "System.Range", "int"),
91+
];
92+
93+
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + OptionStub, expectedDiagnostics);
94+
}
95+
}

Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
### New Rules
55
Rule ID | Category | Severity | Notes
66
--------|----------|----------|-------
7+
λ1010 | Funcky | Error | OptionListPatternAnalyzer
78
λ1101 | Funcky | Warning | FunctionalAssertAnalyzer
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<AnalyzerRoslynVersion>4.0.1</AnalyzerRoslynVersion>
4+
<DisableAnalyzerReleaseTracking>true</DisableAnalyzerReleaseTracking>
45
</PropertyGroup>
56
<Import Project="Funcky.Analyzers.targets" />
67
</Project>

Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.targets

+10
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,15 @@
3838
<ItemGroup>
3939
<EmbeddedResource Update="Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
4040
</ItemGroup>
41+
<PropertyGroup Condition="'$(DisableAnalyzerReleaseTracking)' == 'true'">
42+
<!-- RS2008: Enable analyzer release tracking -->
43+
<NoWarn>$(NoWarn);RS2008</NoWarn>
44+
</PropertyGroup>
45+
<Target Name="_FunckyRemoveAnalyzerReleasesFiles" BeforeTargets="CoreCompile" Condition="'$(DisableAnalyzerReleaseTracking)' == 'true'">
46+
<ItemGroup>
47+
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md" />
48+
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md" />
49+
</ItemGroup>
50+
</Target>
4151
<Import Project="..\Funcky.Analyzers.Package\Packing.targets" />
4252
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#if ROSLYN_4_4_0
2+
using System.Collections.Immutable;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using Microsoft.CodeAnalysis.Operations;
6+
7+
namespace Funcky.Analyzers;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class OptionListPatternAnalyzer : DiagnosticAnalyzer
11+
{
12+
public static readonly DiagnosticDescriptor OptionHasZeroOrOneElements = new(
13+
id: $"{DiagnosticName.Prefix}{DiagnosticName.Usage}10",
14+
title: "An option has either zero or one elements",
15+
messageFormat: "An option has either zero or one elements, testing for more elements will never match",
16+
category: nameof(Funcky),
17+
DiagnosticSeverity.Error,
18+
isEnabledByDefault: true);
19+
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(OptionHasZeroOrOneElements);
21+
22+
public override void Initialize(AnalysisContext context)
23+
{
24+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
25+
context.EnableConcurrentExecution();
26+
context.RegisterCompilationStartAction(OnCompilationStart);
27+
}
28+
29+
private static void OnCompilationStart(CompilationStartAnalysisContext context)
30+
{
31+
if (context.Compilation.GetOptionOfTType() is { } optionOfTType)
32+
{
33+
context.RegisterOperationAction(AnalyzeListPattern(optionOfTType), OperationKind.ListPattern);
34+
}
35+
}
36+
37+
private static Action<OperationAnalysisContext> AnalyzeListPattern(INamedTypeSymbol optionOfTType)
38+
=> context
39+
=>
40+
{
41+
var operation = (IListPatternOperation)context.Operation;
42+
if (SymbolEqualityComparer.Default.Equals(operation.InputType.OriginalDefinition, optionOfTType)
43+
&& operation.Patterns.Length > 1)
44+
{
45+
context.ReportDiagnostic(Diagnostic.Create(OptionHasZeroOrOneElements, operation.Syntax.GetLocation()));
46+
}
47+
};
48+
}
49+
#endif

0 commit comments

Comments
 (0)