From 6d4717cdea7d864181d365b41d215909781309be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Thu, 6 Feb 2025 15:15:36 +0100 Subject: [PATCH 1/2] Disallow option list pattern with more than one element --- .../OptionListPatternTest.cs | 95 +++++++++++++++++++ .../AnalyzerReleases.Unshipped.md | 1 + .../OptionListPatternAnalyzer.cs | 49 ++++++++++ 3 files changed, 145 insertions(+) create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/OptionListPatternTest.cs create mode 100644 Funcky.Analyzers/Funcky.Analyzers/OptionListPatternAnalyzer.cs diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/OptionListPatternTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/OptionListPatternTest.cs new file mode 100644 index 00000000..9971249b --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/OptionListPatternTest.cs @@ -0,0 +1,95 @@ +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier; + +namespace Funcky.Analyzers.Test; + +public sealed class OptionListPatternTest +{ + // language=csharp + private const string OptionStub = + """ + namespace Funcky.Monads + { + public readonly struct Option + { + 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 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 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 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); + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md b/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md index 417828f3..ac2a3b55 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md +++ b/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md @@ -4,4 +4,5 @@ ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- +λ1010 | Funcky | Error | OptionListPatternAnalyzer λ1101 | Funcky | Warning | FunctionalAssertAnalyzer diff --git a/Funcky.Analyzers/Funcky.Analyzers/OptionListPatternAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/OptionListPatternAnalyzer.cs new file mode 100644 index 00000000..334b6d43 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/OptionListPatternAnalyzer.cs @@ -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 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 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 From b1b6714886be804b9f589025e59f7b77d47acb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Thu, 6 Feb 2025 15:15:56 +0100 Subject: [PATCH 2/2] Disable analyzer release tracking for older roslyn version --- .../Funcky.Analyzers/Funcky.Analyzers.Roslyn4.0.csproj | 1 + .../Funcky.Analyzers/Funcky.Analyzers.targets | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.Roslyn4.0.csproj b/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.Roslyn4.0.csproj index c9fb12f3..c05430f8 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.Roslyn4.0.csproj +++ b/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.Roslyn4.0.csproj @@ -1,6 +1,7 @@ 4.0.1 + true diff --git a/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.targets b/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.targets index b1bea80b..c1675f8c 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.targets +++ b/Funcky.Analyzers/Funcky.Analyzers/Funcky.Analyzers.targets @@ -38,5 +38,15 @@ + + + $(NoWarn);RS2008 + + + + + + +