From 396332225ebae6e19d48745368591b1c5cc699f5 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Fri, 30 Oct 2026 09:54:00 +0100 Subject: [PATCH] =?UTF-8?q?Remove=20the=20classic=20Sequence=20and=20Funct?= =?UTF-8?q?ionalAssert=20forms=20and=20retire=20=CE=BB1202=20and=20=CE=BB1?= =?UTF-8?q?203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static class Funcky.Sequence goes; its constructors live on as the static extension members of System.Linq.Enumerable that 3.8.0 added. λ1001 now proposes Enumerable.Return and λ1013 reports Enumerable.Return where a collection expression fits. Funcky.Xunit 3.0.0 and Funcky.Xunit.v3 2.0.0 drop the FunctionalAssert class in favour of the Assert extension members, and the failure label becomes Assert.Some() to match xunit's own Assert.Equal() Failure. λ1202 and λ1203 retire from Funcky.Analyzers, since the forms they pointed away from no longer compile; λ1201 stays as an opt-in style rule and loses its C# 14 language gate, which the .NET 10 SDK baseline makes moot. The PublicAPI files record the removals, the tests use the surviving forms, and the documentation describes the upgrade path through the 3.8.0 rules. Closes #1003 Closes #1019 Co-Authored-By: Claude Fable 5.1 --- Documentation/src/SUMMARY.md | 2 - .../src/analyzer-rules/analyzer-rules.md | 13 +- .../src/analyzer-rules/\316\2731001.md" | 8 +- .../src/analyzer-rules/\316\2731013.md" | 20 +- .../src/analyzer-rules/\316\2731101.md" | 17 +- .../src/analyzer-rules/\316\2731201.md" | 7 +- .../src/analyzer-rules/\316\2731202.md" | 38 ---- .../src/analyzer-rules/\316\2731203.md" | 30 --- .../src/case-studies/calendar-code.md | 2 +- Documentation/src/case-studies/calendar.md | 18 +- .../src/case-studies/case-studies.md | 2 +- Documentation/src/case-studies/csv-import.md | 2 +- .../adjacent-group-by.md | 2 +- .../aggregate-or-none.md | 2 +- .../enumerable-extensions/average-or-none.md | 4 +- .../cartesian-product.md | 4 +- .../src/enumerable-extensions/chunk.md | 4 +- .../element-at-or-none.md | 2 +- .../enumerable-extensions/first-or-none.md | 2 +- .../src/enumerable-extensions/flatten.md | 10 +- .../src/enumerable-extensions/interleave.md | 6 +- .../src/enumerable-extensions/intersperse.md | 4 +- .../enumerable-extensions/join-to-string.md | 2 +- .../src/enumerable-extensions/last-or-none.md | 2 +- .../src/enumerable-extensions/max-or-none.md | 2 +- .../src/enumerable-extensions/memoize.md | 2 +- .../src/enumerable-extensions/merge.md | 6 +- .../enumerable-extensions/min-by-or-none.md | 2 +- .../src/enumerable-extensions/min-or-none.md | 4 +- .../src/enumerable-extensions/pairwise.md | 2 +- .../src/enumerable-extensions/power-set.md | 2 +- .../src/enumerable-extensions/scan.md | 2 +- .../src/enumerable-extensions/sequence.md | 4 +- .../enumerable-extensions/single-or-none.md | 6 +- .../enumerable-extensions/sliding-window.md | 2 +- .../src/enumerable-extensions/split.md | 2 +- .../src/enumerable-extensions/transpose.md | 6 +- .../src/enumerable-extensions/traverse.md | 4 +- .../enumerable-extensions/where-not-null.md | 4 +- .../src/enumerable-extensions/where-select.md | 4 +- .../src/enumerable-extensions/with-index.md | 2 +- .../enumerable-extensions/with-previous.md | 2 +- .../src/enumerable-extensions/zip-longest.md | 4 +- .../src/functional-helpers/compose.md | 2 +- Documentation/src/sequence/sequence.md | 64 +++--- Documentation/src/stream-extensions.md | 2 +- Documentation/src/xunit.md | 20 +- .../CodeFixResources.Designer.cs | 2 +- .../CodeFixResources.resx | 2 +- .../EnumerableRepeatOnceCodeFix.cs | 21 +- ...erEnumerableSequenceConstructorsCodeFix.cs | 43 ---- .../PreferXunitAssertCodeFix.cs | 43 ---- .../EnumerableRepeatOnceTest.cs | 16 +- ...referEnumerableSequenceConstructorsTest.cs | 136 ------------ .../PreferStaticParseOrNoneTest.cs | 44 ---- .../PreferXunitAssertTest.cs | 198 ------------------ .../SequenceReturnCollectionExpressionTest.cs | 136 +++++++----- .../TestCode/RepeatOnce.expected | 10 +- .../TestCode/RepeatOnce.input | 8 +- .../TestCode/RepeatOnceFlipped.expected | 10 +- .../TestCode/RepeatOnceFlipped.input | 8 +- ...peatOnceMissingEnumerableExtensions.input} | 0 .../TestCode/RepeatOnceQualification.expected | 23 +- .../TestCode/RepeatOnceQualification.input | 23 +- .../TestCode/RepeatOnceWithConstant.expected | 10 +- .../TestCode/RepeatOnceWithConstant.input | 8 +- .../AnalyzerReleases.Unshipped.md | 6 + .../SingleElementCollectionExpression.cs | 2 +- .../Funcky.Analyzers/DiagnosticName.cs | 4 +- .../EnumerableRepeatOnceAnalyzer.cs | 6 +- .../ExtensionForms/ExtensionMemberSupport.cs | 14 +- ...rEnumerableSequenceConstructorsAnalyzer.cs | 92 -------- .../PreferStaticParseOrNoneAnalyzer.cs | 1 - .../PreferXunitAssertAnalyzer.cs | 59 ------ .../FunckyWellKnownTypeNames.cs | 4 - .../Funcky.Analyzers/Resources.Designer.cs | 4 +- .../Funcky.Analyzers/Resources.resx | 4 +- ...uenceReturnCollectionExpressionAnalyzer.cs | 27 +-- Funcky.Analyzers/readme.md | 3 +- Funcky.Test/Extensions/SequenceTest.cs | 16 +- Funcky.Test/Sequence/ConcatTest.cs | 8 +- Funcky.Test/Sequence/CycleMaterializedTest.cs | 10 +- Funcky.Test/Sequence/CycleRangeTest.cs | 18 +- Funcky.Test/Sequence/CycleTest.cs | 8 +- .../Sequence/EnumerableConstructorsTest.cs | 4 +- .../Sequence/RepeatMaterializedTest.cs | 10 +- Funcky.Test/Sequence/RepeatRangeTest.cs | 22 +- Funcky.Test/Sequence/ReturnTest.cs | 14 +- Funcky.Test/Sequence/SuccessorsTest.cs | 24 +-- .../ErrorTest.cs | 20 +- .../LeftTest.cs | 20 +- .../NoneTest.cs | 10 +- .../OkTest.cs | 20 +- .../RightTest.cs | 20 +- .../SomeTest.cs | 20 +- .../StackTraceTest.cs | 16 +- .../Extensions/AssertExtensionsTest.cs | 13 +- Funcky.Xunit.v3/Funcky.Xunit.v3.csproj | 2 +- .../PublicAPI/all/PublicAPI.Unshipped.txt | 12 ++ Funcky.Xunit.v3/package.md | 31 +-- ...odHasOverloadWithExpectedValueAttribute.cs | 4 +- Funcky.Xunit/Extensions/AssertExtensions.cs | 3 +- Funcky.Xunit/Funcky.Xunit.csproj | 2 +- Funcky.Xunit/FunctionalAssert/Error.cs | 47 ----- Funcky.Xunit/FunctionalAssert/Left.cs | 50 ----- Funcky.Xunit/FunctionalAssert/None.cs | 27 --- Funcky.Xunit/FunctionalAssert/Ok.cs | 48 ----- Funcky.Xunit/FunctionalAssert/Right.cs | 50 ----- Funcky.Xunit/FunctionalAssert/Some.cs | 49 ----- Funcky.Xunit/FunctionalAssertException.cs | 5 +- .../PublicAPI/all/PublicAPI.Unshipped.txt | 12 ++ Funcky.Xunit/package.md | 31 +-- Funcky.Xunit/readme.md | 14 +- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 16 ++ .../PublicAPI/net11.0/PublicAPI.Unshipped.txt | 16 ++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 16 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 16 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 16 ++ .../netstandard2.1/PublicAPI.Unshipped.txt | 16 ++ Funcky/Sequence/Sequence.Concat.cs | 14 -- Funcky/Sequence/Sequence.Cycle.cs | 9 - Funcky/Sequence/Sequence.CycleMaterialized.cs | 9 - Funcky/Sequence/Sequence.CycleRange.cs | 9 - Funcky/Sequence/Sequence.FromNullable.cs | 16 -- .../Sequence/Sequence.RepeatMaterialized.cs | 9 - Funcky/Sequence/Sequence.RepeatRange.cs | 9 - Funcky/Sequence/Sequence.Return.cs | 14 -- Funcky/Sequence/Sequence.Successors.cs | 27 --- README.md | 6 +- changelog.md | 14 ++ 130 files changed, 639 insertions(+), 1573 deletions(-) delete mode 100644 "Documentation/src/analyzer-rules/\316\2731202.md" delete mode 100644 "Documentation/src/analyzer-rules/\316\2731203.md" delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferEnumerableSequenceConstructorsCodeFix.cs delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferXunitAssertCodeFix.cs delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/PreferEnumerableSequenceConstructorsTest.cs delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/PreferXunitAssertTest.cs rename Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/{RepeatOnceMissingSequenceType.input => RepeatOnceMissingEnumerableExtensions.input} (100%) delete mode 100644 Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferEnumerableSequenceConstructorsAnalyzer.cs delete mode 100644 Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferXunitAssertAnalyzer.cs rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/ErrorTest.cs (58%) rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/LeftTest.cs (57%) rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/NoneTest.cs (52%) rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/OkTest.cs (58%) rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/RightTest.cs (56%) rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/SomeTest.cs (58%) rename Funcky.Xunit.Test/{FunctionalAssertClass => AssertClass}/StackTraceTest.cs (59%) delete mode 100644 Funcky.Xunit/FunctionalAssert/Error.cs delete mode 100644 Funcky.Xunit/FunctionalAssert/Left.cs delete mode 100644 Funcky.Xunit/FunctionalAssert/None.cs delete mode 100644 Funcky.Xunit/FunctionalAssert/Ok.cs delete mode 100644 Funcky.Xunit/FunctionalAssert/Right.cs delete mode 100644 Funcky.Xunit/FunctionalAssert/Some.cs delete mode 100644 Funcky/Sequence/Sequence.Concat.cs delete mode 100644 Funcky/Sequence/Sequence.Cycle.cs delete mode 100644 Funcky/Sequence/Sequence.CycleMaterialized.cs delete mode 100644 Funcky/Sequence/Sequence.CycleRange.cs delete mode 100644 Funcky/Sequence/Sequence.FromNullable.cs delete mode 100644 Funcky/Sequence/Sequence.RepeatMaterialized.cs delete mode 100644 Funcky/Sequence/Sequence.RepeatRange.cs delete mode 100644 Funcky/Sequence/Sequence.Return.cs delete mode 100644 Funcky/Sequence/Sequence.Successors.cs diff --git a/Documentation/src/SUMMARY.md b/Documentation/src/SUMMARY.md index 8323a5569..7bafb5bc9 100644 --- a/Documentation/src/SUMMARY.md +++ b/Documentation/src/SUMMARY.md @@ -66,7 +66,5 @@ * [λ1018](./analyzer-rules/λ1018.md) * [λ1101](./analyzer-rules/λ1101.md) * [λ1201](./analyzer-rules/λ1201.md) - * [λ1202](./analyzer-rules/λ1202.md) - * [λ1203](./analyzer-rules/λ1203.md) [Changelog](changelog.md) diff --git a/Documentation/src/analyzer-rules/analyzer-rules.md b/Documentation/src/analyzer-rules/analyzer-rules.md index 594b7849e..64286e9de 100644 --- a/Documentation/src/analyzer-rules/analyzer-rules.md +++ b/Documentation/src/analyzer-rules/analyzer-rules.md @@ -21,10 +21,11 @@ This is the same split the .NET libraries themselves use: usage diagnostics ship ### Rule numbers Rule IDs are stable identifiers: once a rule has shipped, its number never changes, because projects configure and suppress rules by ID. The number range therefore records where a rule was *born*, not where it lives today: -`λ0xxx` rules started as built-in rules, `λ1xxx` rules started in `Funcky.Analyzers`, and `λ11xx` rules concern the -`FunctionalAssert` helpers from the [`Funcky.Xunit`] package, and `λ12xx` rules point at the C# 14 extension forms that -3.8.0 added next to the classic API. The tables below are the authority on which package -contains a rule. +`λ0xxx` rules started as built-in rules, `λ1xxx` rules started in `Funcky.Analyzers`, `λ11xx` rules concern the +assertions of the [`Funcky.Xunit`] package (born as `FunctionalAssert` rules), and `λ12xx` rules point at the C# 14 +extension forms that 3.8.0 added next to the classic API. λ1202 and λ1203 retired with Funcky 4.0.0, which removed the +classic `Sequence` and `FunctionalAssert` forms they pointed away from; the numbers are not reused. The tables below +are the authority on which package contains a rule. ## Built-in rules @@ -51,7 +52,7 @@ contains a rule. | [λ1008](./λ1008.md) | Warning | Prefer `ToNullable` over `Match` | | [λ1011](./λ1011.md) | Warning | Prefer `Select` over `Match` | | [λ1012](./λ1012.md) | Warning | Prefer the `…OrNone` method over `…OrDefault` with a null check | -| [λ1013](./λ1013.md) | Info | `Sequence.Return` can be replaced with a collection expression | +| [λ1013](./λ1013.md) | Info | `Enumerable.Return` can be replaced with a collection expression | | [λ1014](./λ1014.md) | Warning | Throw expression in an eagerly evaluated fallback | | [λ1015](./λ1015.md) | Info | Fallback is computed eagerly | | [λ1016](./λ1016.md) | Warning | Pass the fallback value directly | @@ -59,8 +60,6 @@ contains a rule. | [λ1018](./λ1018.md) | Warning | Prefer `Flatten` over `SelectMany` with the identity function | | [λ1101](./λ1101.md) | Warning | Assert can be simplified | | [λ1201](./λ1201.md) | Warning | Prefer the static `ParseOrNone` member on the parsed type | -| [λ1202](./λ1202.md) | Warning | Prefer the `Enumerable` form of the `Sequence` constructor | -| [λ1203](./λ1203.md) | Warning | Prefer the assertion on `Assert` over `FunctionalAssert` | ## Removed rules diff --git "a/Documentation/src/analyzer-rules/\316\2731001.md" "b/Documentation/src/analyzer-rules/\316\2731001.md" index 1abe291c4..5a09eccbe 100644 --- "a/Documentation/src/analyzer-rules/\316\2731001.md" +++ "b/Documentation/src/analyzer-rules/\316\2731001.md" @@ -1,16 +1,16 @@ # λ1001: Use of `Enumerable.Repeat` for a single element -`Enumerable.Repeat(value, 1)` should be replaced with `Sequence.Return(value)`. +`Enumerable.Repeat(value, 1)` should be replaced with `Enumerable.Return(value)`. ## Cause `Enumerable.Repeat` is called with a constant count of `1`. ## Reason for rule Repeating a value exactly once is a roundabout way of saying "a sequence containing this one element". -Funcky provides `Sequence.Return` for precisely this purpose. It states the intent directly and mirrors +Funcky provides `Enumerable.Return` for precisely this purpose. It states the intent directly and mirrors the `Return` operation of the other monads in the library. ## How to fix violations -Replace the call with `Sequence.Return(value)`. A code fix is available. +Replace the call with `Enumerable.Return(value)`. A code fix is available. ## Examples @@ -21,7 +21,7 @@ IEnumerable greeting = Enumerable.Repeat("hello", 1); ### Allowed ```cs -IEnumerable greeting = Sequence.Return("hello"); +IEnumerable greeting = Enumerable.Return("hello"); // Repeating a value more than once is fine. IEnumerable echo = Enumerable.Repeat("hello", 3); diff --git "a/Documentation/src/analyzer-rules/\316\2731013.md" "b/Documentation/src/analyzer-rules/\316\2731013.md" index 6c6583f51..fb981c8e4 100644 --- "a/Documentation/src/analyzer-rules/\316\2731013.md" +++ "b/Documentation/src/analyzer-rules/\316\2731013.md" @@ -1,23 +1,23 @@ -# λ1013: `Sequence.Return` can be replaced with a collection expression -A single-element `Sequence.Return(x)` in a position that supplies a collection target type is the collection expression +# λ1013: `Enumerable.Return` can be replaced with a collection expression +A single-element `Enumerable.Return(x)` in a position that supplies a collection target type is the collection expression `[x]`. ## Cause -`Sequence.Return` is called with one element where the compiler knows the target type: an argument, a `return`, an +`Enumerable.Return` is called with one element where the compiler knows the target type: an argument, a `return`, an expression body, a cast, an assignment or an explicitly typed initializer whose type is `IEnumerable`, `IReadOnlyCollection` or `IReadOnlyList`. The rule only fires when the rewrite changes nothing but the spelling: * the file is compiled as C# 12 or later, which collection expressions need; -* the call is the single-element overload, not the `params` one. `Sequence.Return(array)` through the `params` overload +* the call is the single-element overload, not the `params` one. `Enumerable.Return(array)` through the `params` overload would become `[array]`, a one-element list of arrays; * the collection expression has the same element type and binds to the same member, so an overload that only a collection expression would pick is left alone; * the call is not inside an expression tree. -`var x = Sequence.Return(1)` is not reported, because `var x = [1]` does not compile, and neither is `Sequence.Return` -as the receiver of a LINQ chain: there is no target type to speak of, and `Sequence.Return` stays the right tool. +`var x = Enumerable.Return(1)` is not reported, because `var x = [1]` does not compile, and neither is `Enumerable.Return` +as the receiver of a LINQ chain: there is no target type to speak of, and `Enumerable.Return` stays the right tool. ## Reason for rule A collection expression is the language's own spelling for a literal collection; it needs no helper and reads like the @@ -32,10 +32,10 @@ without a left operand is parenthesized so that it does not turn into a spread. ### Disallowed ```cs static IEnumerable Example() - => Sequence.Return(42); + => Enumerable.Return(42); static void Example() - => Consume(Sequence.Return("tag")); + => Consume(Enumerable.Return("tag")); static void Consume(IReadOnlyList tags) { } ``` @@ -50,9 +50,9 @@ static void Example() // No target type: the receiver of a LINQ chain. static IEnumerable Example() - => Sequence.Return(42).Concat(More()); + => Enumerable.Return(42).Concat(More()); // The element type differs from the target, so this is not the same collection. static IEnumerable Example() - => Sequence.Return("tag"); + => Enumerable.Return("tag"); ``` diff --git "a/Documentation/src/analyzer-rules/\316\2731101.md" "b/Documentation/src/analyzer-rules/\316\2731101.md" index fd71108fb..cbac57424 100644 --- "a/Documentation/src/analyzer-rules/\316\2731101.md" +++ "b/Documentation/src/analyzer-rules/\316\2731101.md" @@ -1,14 +1,15 @@ # λ1101: Assert can be simplified -`Assert.Equal(expected, FunctionalAssert.Some(option))` should be written as `FunctionalAssert.Some(expected, option)`. +`Assert.Equal(expected, Assert.Some(option))` should be written as `Assert.Some(expected, option)`. ## Cause -The result of a `FunctionalAssert` method that unwraps a monad (`Some`, `Ok`, `Left`, `Right`) is passed as the -`actual` argument to xUnit's `Assert.Equal(T expected, T actual)`. +The result of a Funcky.Xunit assertion that unwraps a monad (`Assert.Some`, `Assert.Ok`, `Assert.Left`, `Assert.Right`) +is passed as the `actual` argument to xUnit's `Assert.Equal(T expected, T actual)`. -This rule ships with `Funcky.Analyzers` but only applies to code using the [`Funcky.Xunit`](https://www.nuget.org/packages/Funcky.Xunit) package. +This rule ships with `Funcky.Analyzers` but only applies to code using the [`Funcky.Xunit`](https://www.nuget.org/packages/Funcky.Xunit) +or [`Funcky.Xunit.v3`](https://www.nuget.org/packages/Funcky.Xunit.v3) package. ## Reason for rule -Each of these `FunctionalAssert` methods has an overload that takes the expected value directly. +Each of these assertions has an overload that takes the expected value directly. The single call is shorter and produces a better failure message, because the assertion knows both the expected value and the monad it was extracted from. @@ -23,7 +24,7 @@ Use the overload that accepts the expected value as its first argument. A code f public void ReturnsTheAnswer() { Option option = Option.Some(42); - Assert.Equal(42, FunctionalAssert.Some(option)); + Assert.Equal(42, Assert.Some(option)); } ``` @@ -33,7 +34,7 @@ public void ReturnsTheAnswer() public void ReturnsTheAnswer() { Option option = Option.Some(42); - FunctionalAssert.Some(42, option); + Assert.Some(42, option); } // Unwrapping and then asserting something other than equality is fine. @@ -41,7 +42,7 @@ public void ReturnsTheAnswer() public void ReturnsAPositiveNumber() { Option option = Option.Some(42); - var value = FunctionalAssert.Some(option); + var value = Assert.Some(option); Assert.True(value > 0); } ``` diff --git "a/Documentation/src/analyzer-rules/\316\2731201.md" "b/Documentation/src/analyzer-rules/\316\2731201.md" index bad5265f7..c306c63f0 100644 --- "a/Documentation/src/analyzer-rules/\316\2731201.md" +++ "b/Documentation/src/analyzer-rules/\316\2731201.md" @@ -4,7 +4,7 @@ it parses. `text.ParseInt32OrNone()` is `int.ParseOrNone(text)`, which reads lik ## Cause A `Parse…OrNone` extension of `ParseExtensions` is called on a `string`, a `ReadOnlySpan` or a `ReadOnlySpan`, -the file is compiled as C# 14 or later, and the static member on the parsed type binds for the same arguments. The rule +and the static member on the parsed type binds for the same arguments. The rule covers the generated members (`ParseInt32OrNone`, `ParseExactDateTimeOrNone`, …), the enum form (`ParseEnumOrNone`) and the generic forms (`ParseOrNone`, `ParseNumberOrNone`); the parsed type comes from the `Option` the extension returns. @@ -13,8 +13,9 @@ A method group (`texts.Select(ParseExtensions.ParseInt32OrNone)`) is not reporte method-group form of the same shape. ## Reason for rule -The static form is the one that mirrors the base class library, and it is the form that survives if 4.0.0 retires the -`string` family. Adopting it in one sweep is what the code fix is for. +The static form is the one that mirrors the base class library. Both forms stay: the fluent `string` extension has its +own place in a pipeline, so this rule is an opt-in style preference, and adopting the static form in one sweep is what +the code fix is for. ## How to fix violations Call the static member on the parsed type with the text as the first argument. The code fix does this, keeps the other diff --git "a/Documentation/src/analyzer-rules/\316\2731202.md" "b/Documentation/src/analyzer-rules/\316\2731202.md" deleted file mode 100644 index ec650b6e4..000000000 --- "a/Documentation/src/analyzer-rules/\316\2731202.md" +++ /dev/null @@ -1,38 +0,0 @@ -# λ1202: Prefer the `Enumerable` form of the `Sequence` constructor -Since 3.8.0 the constructors of `Funcky.Sequence` also exist as static extension members of `System.Linq.Enumerable`, -next to `Enumerable.Range` and `Enumerable.Repeat`: `Sequence.Successors(1, n => n + 1)` is -`Enumerable.Successors(1, n => n + 1)`. - -## Cause -A constructor of `Sequence` (`Return`, `FromNullable`, `Successors`, `Cycle`, `CycleRange`, `CycleMaterialized`, -`RepeatRange`, `RepeatMaterialized`, `Concat`) is called, the file is compiled as C# 14 or later, and the same call with -`Enumerable` as the receiver binds to the `Enumerable` block of `EnumerableExtensions`. A call that would bind to an -overload of `Enumerable` itself is left alone, and so is one inside an expression tree. - -A method group (`values.Select(Sequence.Return)`) is not reported: a static extension member has no method-group form of -the same shape. - -## Reason for rule -The constructors sit where the base class library keeps its own sequence constructors, so a reader finds them without -knowing Funcky's `Sequence` class. The rule is the upgrade path should a later major version retire `Sequence`. - -## How to fix violations -Replace the `Sequence` receiver with `Enumerable`. The code fix does this, keeps the arguments and type arguments, and -adds `using Funcky.Extensions;` when the extension members are not in scope yet. It supports fix-all. - -## Examples - -### Disallowed -```cs -IEnumerable powersOfTwo = Sequence.Successors(1, n => n * 2); -IReadOnlyList tags = Sequence.Return("a", "b"); -``` - -### Allowed -```cs -IEnumerable powersOfTwo = Enumerable.Successors(1, n => n * 2); -IReadOnlyList tags = Enumerable.Return("a", "b"); - -// A method group has no static counterpart of the same shape. -IEnumerable> singletons = values.Select(Sequence.Return); -``` diff --git "a/Documentation/src/analyzer-rules/\316\2731203.md" "b/Documentation/src/analyzer-rules/\316\2731203.md" deleted file mode 100644 index e8f469f97..000000000 --- "a/Documentation/src/analyzer-rules/\316\2731203.md" +++ /dev/null @@ -1,30 +0,0 @@ -# λ1203: Prefer the assertion on `Assert` over `FunctionalAssert` -Since Funcky.Xunit 2.3.0 and Funcky.Xunit.v3 1.1.0 the assertions of `FunctionalAssert` are also static extension -members of xunit's `Assert`: `FunctionalAssert.Some(option)` is `Assert.Some(option)`, found where the other -assertions live. - -## Cause -A method of `FunctionalAssert` is called, the file is compiled as C# 14 or later, and `AssertExtensions` from a -Funcky.Xunit package that has the `Assert` members is referenced. - -## Reason for rule -One `Assert` class for every assertion in a test is easier to discover and to read than two, and the rule is the upgrade -path should a later major version retire `FunctionalAssert`. - -## How to fix violations -Replace the `FunctionalAssert` receiver with `Assert`. The code fix does this, keeps the arguments and type arguments, -and adds `using Funcky.Extensions;` when the extension members are not in scope yet. It supports fix-all. - -## Examples - -### Disallowed -```cs -FunctionalAssert.Some(42, option); -var value = FunctionalAssert.Ok(result); -``` - -### Allowed -```cs -Assert.Some(42, option); -var value = Assert.Ok(result); -``` diff --git a/Documentation/src/case-studies/calendar-code.md b/Documentation/src/case-studies/calendar-code.md index 346d76eee..a84e9358c 100644 --- a/Documentation/src/case-studies/calendar-code.md +++ b/Documentation/src/case-studies/calendar-code.md @@ -41,7 +41,7 @@ internal class Program .AndThen(ParseExtensions.ParseInt32OrNone); private static string CreateCalendarString(int year) - => Sequence.Successors(JanuaryFirst(year), NextDay) + => Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .AdjacentGroupBy(day => day.Month) .Select(LayoutMonth) diff --git a/Documentation/src/case-studies/calendar.md b/Documentation/src/case-studies/calendar.md index 0c9426bea..5a0bbf31d 100644 --- a/Documentation/src/case-studies/calendar.md +++ b/Documentation/src/case-studies/calendar.md @@ -53,10 +53,10 @@ There are obviously many ways to achieve this, you could write your own Implemen We want to avoid to write such a function and take advantage of the numerous generators available in Funcky. -`Sequence.Successors` creates an infinite sequence of days, starting with January first of the given year. We take all the values in the same year, so this sequence should yield all the days of a year. +`Enumerable.Successors` creates an infinite sequence of days, starting with January first of the given year. We take all the values in the same year, so this sequence should yield all the days of a year. ```cs -return Sequence.Successors(JanuaryFirst(year), NextDay) +return Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)); ``` @@ -83,7 +83,7 @@ This makes the main body of the code semantic very easy to understand. All the h LINQ offers you the GroupBy function which we could use easily in this case. This way we would have 12 groups, in each group we would have all the days of one month of the given year. ```cs -return Sequence.Successors(JanuaryFirst(year), NextDay) +return Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .GroupBy(d => d.Month) ``` @@ -98,7 +98,7 @@ Often that is exactly what you want, but in this case if we think of an endless That is why Funcky offers the extension function `AdjacentGroupBy`. It is a fully lazy, forward only `GroupBy`. Exactly what we need here. ```cs -return Sequence.Successors(JanuaryFirst(year), NextDay) +return Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .AdjacentGroupBy(d => d.Month); ``` @@ -112,7 +112,7 @@ Since we have now all the days of the same month, we want to create a layout for Currently we have a sequence of months. To transform the inner element, from a sequence of days into something else, we need to make a projection which is done with `Select` in C#. ```cs -return Sequence.Successors(JanuaryFirst(year), NextDay) +return Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .AdjacentGroupBy(d => d.Month); .Select(LayoutMonth) @@ -152,7 +152,7 @@ private static IEnumerable LayoutMonth(IEnumerable month) This is a fine way to do this, it is very easy to read, but the foreach is really annoying. We already have an `IEnumerable`, we just want to add it between the first and the last line. But with yield you are often limited to very procedural constructs. -Often you can avoid that, for simple cases we have Sequence.Return, Concat and other helpers, in this case though the nicest way is probably creating an ImmutableList, because the syntax allows to combine ranges and single items elegantly. +Often you can avoid that, for simple cases we have Enumerable.Return, Concat and other helpers, in this case though the nicest way is probably creating an ImmutableList, because the syntax allows to combine ranges and single items elegantly. ```cs private static IEnumerable LayoutMonth(IEnumerable month) @@ -281,7 +281,7 @@ Chunk is grouping a sequence into multiple sequences of the same length. In our const MonthsPerRow = 3; private static string CreateCalendarString(int year) - => Sequence.Successors(JanuaryFirst(year), NextDay) + => Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .AdjacentGroupBy(day => day.Month) .Select(LayoutMonth) @@ -316,7 +316,7 @@ Funcky has a lazy `Transpose` extension function for `IEnumerable ```cs private static string CreateCalendarString(int year) - => Sequence.Successors(JanuaryFirst(year), NextDay) + => Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .AdjacentGroupBy(day => day.Month) .Select(LayoutMonth) @@ -344,7 +344,7 @@ I think it is obvious that at this point, we are done. We just have to join the ```cs private static string CreateCalendarString(int year) - => Sequence.Successors(JanuaryFirst(year), NextDay) + => Enumerable.Successors(JanuaryFirst(year), NextDay) .TakeWhile(IsSameYear(year)) .AdjacentGroupBy(day => day.Month) .Select(LayoutMonth) diff --git a/Documentation/src/case-studies/case-studies.md b/Documentation/src/case-studies/case-studies.md index e1cb99e44..36d32053e 100644 --- a/Documentation/src/case-studies/case-studies.md +++ b/Documentation/src/case-studies/case-studies.md @@ -9,7 +9,7 @@ Each study is self-contained and comes with the full program, which compiles aga | Case study | Task | Features | |---------------------------------------------------------|-----------------------------------------------------------|---------------------------------------------------------------------------------------| | [Simplify `if null` by using an `Option`](./if-null-to-option.md) | Refactor a chain of null checks step by step | `Option`, `FromNullable`, `Select`, `AndThen`, `GetOrElse` | -| [Formatting a calendar page](./calendar.md) | Lay out a yearly calendar as a streaming pipeline | `Sequence.Successors`, `AdjacentGroupBy`, `Chunk`, `Transpose`, `JoinToString`, `Curry` | +| [Formatting a calendar page](./calendar.md) | Lay out a yearly calendar as a streaming pipeline | `Enumerable.Successors`, `AdjacentGroupBy`, `Chunk`, `Transpose`, `JoinToString`, `Curry` | | [Importing a CSV file](./csv-import.md) | Parse and validate records, report or reject bad lines | `ParseOrNone`, `Either`, LINQ over monads, `Partition`, `Sequence`, `WithIndex`, `MaxByOrNone`, `Pairwise`, `InclusiveScan` | If you have a task that you solved with Funcky and that shows an aspect none of these cover, a new case study is a diff --git a/Documentation/src/case-studies/csv-import.md b/Documentation/src/case-studies/csv-import.md index 2d4e3ee82..4a87d3346 100644 --- a/Documentation/src/case-studies/csv-import.md +++ b/Documentation/src/case-studies/csv-import.md @@ -151,7 +151,7 @@ static string Report(IReadOnlyList transactions) .JoinToString(Environment.NewLine); ``` -`Sequence.Return` builds a sequence from its arguments and `JoinToString` is the `string.Join` you can put at +`Enumerable.Return` builds a sequence from its arguments and `JoinToString` is the `string.Join` you can put at the end of a pipeline. Monthly totals use `AdjacentGroupBy`, the same forward-only grouping as in the diff --git a/Documentation/src/enumerable-extensions/adjacent-group-by.md b/Documentation/src/enumerable-extensions/adjacent-group-by.md index 11cf35bb5..8d1a39930 100644 --- a/Documentation/src/enumerable-extensions/adjacent-group-by.md +++ b/Documentation/src/enumerable-extensions/adjacent-group-by.md @@ -30,7 +30,7 @@ This is the same operation as Haskell's `groupBy` and Python's `itertools.groupb Collapsing a log into runs of the same severity: ```cs -var entries = Sequence.Return( +var entries = Enumerable.Return( new LogEntry(Level.Info, "started"), new LogEntry(Level.Info, "listening"), new LogEntry(Level.Error, "timeout"), diff --git a/Documentation/src/enumerable-extensions/aggregate-or-none.md b/Documentation/src/enumerable-extensions/aggregate-or-none.md index 3b5c57a76..c974b21ef 100644 --- a/Documentation/src/enumerable-extensions/aggregate-or-none.md +++ b/Documentation/src/enumerable-extensions/aggregate-or-none.md @@ -17,7 +17,7 @@ is the result. ### Example ```cs -var words = Sequence.Return("functional", "programming", "in", "C#"); +var words = Enumerable.Return("functional", "programming", "in", "C#"); words.AggregateOrNone((longest, word) => word.Length > longest.Length ? word : longest); // Some("programming") Enumerable.Empty().AggregateOrNone((a, b) => a + b); // None diff --git a/Documentation/src/enumerable-extensions/average-or-none.md b/Documentation/src/enumerable-extensions/average-or-none.md index 76d7b4600..304cbdd3b 100644 --- a/Documentation/src/enumerable-extensions/average-or-none.md +++ b/Documentation/src/enumerable-extensions/average-or-none.md @@ -29,10 +29,10 @@ The result type follows the BCL: `int` and `long` average to `double`, the float ### Example ```cs -Sequence.Return(3, 1, 4, 1, 6).AverageOrNone(); // Some(3.0) +Enumerable.Return(3, 1, 4, 1, 6).AverageOrNone(); // Some(3.0) Enumerable.Empty().AverageOrNone(); // None -var temperatures = Sequence.Return(Option.Some(20.0), Option.None, Option.Some(22.0)); +var temperatures = Enumerable.Return(Option.Some(20.0), Option.None, Option.Some(22.0)); temperatures.AverageOrNone(); // Some(21.0) orders.AverageOrNone(order => order.Total); // Option diff --git a/Documentation/src/enumerable-extensions/cartesian-product.md b/Documentation/src/enumerable-extensions/cartesian-product.md index 24f1c7bed..1c5999f9d 100644 --- a/Documentation/src/enumerable-extensions/cartesian-product.md +++ b/Documentation/src/enumerable-extensions/cartesian-product.md @@ -37,8 +37,8 @@ it before. Every playing card is a suit combined with a rank: ```cs -var suits = Sequence.Return("♠", "♣", "♥", "♦"); -var ranks = Sequence.Return("2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"); +var suits = Enumerable.Return("♠", "♣", "♥", "♦"); +var ranks = Enumerable.Return("2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"); var deck = from suit in suits diff --git a/Documentation/src/enumerable-extensions/chunk.md b/Documentation/src/enumerable-extensions/chunk.md index e060c44c2..c8d3f9a52 100644 --- a/Documentation/src/enumerable-extensions/chunk.md +++ b/Documentation/src/enumerable-extensions/chunk.md @@ -26,12 +26,12 @@ way to write `Chunk(size).Select(selector)`. ### Example ```cs -var numbers = Sequence.Return(1, 2, 3, 4, 5, 6, 7); +var numbers = Enumerable.Return(1, 2, 3, 4, 5, 6, 7); numbers.Chunk(3); // [[1, 2, 3], [4, 5, 6], [7]] -var magicSquare = Sequence.Return(4, 9, 2, 3, 5, 7, 8, 1, 6); +var magicSquare = Enumerable.Return(4, 9, 2, 3, 5, 7, 8, 1, 6); magicSquare.Chunk(3, row => row.Sum()); // [15, 15, 15] ``` diff --git a/Documentation/src/enumerable-extensions/element-at-or-none.md b/Documentation/src/enumerable-extensions/element-at-or-none.md index d4eec75d1..7ae6ee548 100644 --- a/Documentation/src/enumerable-extensions/element-at-or-none.md +++ b/Documentation/src/enumerable-extensions/element-at-or-none.md @@ -21,7 +21,7 @@ whole sequence. ### Example ```cs -var numbers = Sequence.Return(3, 1, 4, 1, 5); +var numbers = Enumerable.Return(3, 1, 4, 1, 5); numbers.ElementAtOrNone(2); // Some(4) numbers.ElementAtOrNone(5); // None diff --git a/Documentation/src/enumerable-extensions/first-or-none.md b/Documentation/src/enumerable-extensions/first-or-none.md index cb3a648aa..122475860 100644 --- a/Documentation/src/enumerable-extensions/first-or-none.md +++ b/Documentation/src/enumerable-extensions/first-or-none.md @@ -21,7 +21,7 @@ as long as a matching element exists. ### Example ```cs -var numbers = Sequence.Return(3, 1, 4, 1, 5); +var numbers = Enumerable.Return(3, 1, 4, 1, 5); numbers.FirstOrNone(); // Some(3) numbers.FirstOrNone(n => n > 3); // Some(4) diff --git a/Documentation/src/enumerable-extensions/flatten.md b/Documentation/src/enumerable-extensions/flatten.md index 517aa3aa1..3de41a0f7 100644 --- a/Documentation/src/enumerable-extensions/flatten.md +++ b/Documentation/src/enumerable-extensions/flatten.md @@ -16,17 +16,17 @@ IEnumerable Flatten(this IEnumerable> enumerable) `Flatten` is `SelectMany(Identity)`: the inner sequences are enumerated one after the other, in order, lazily. It is the inverse of `Chunk`, and exists because `SelectMany(x => x)` says much less than its name. -The same operation on a known number of sequences is `Sequence.Concat(first, second, third)`, which is the +The same operation on a known number of sequences is `Enumerable.Concat(first, second, third)`, which is the variadic form of LINQ's `Concat`. ### Example ```cs -var pages = Sequence.Return( - Sequence.Return(1, 2, 3), - Sequence.Return(4, 5), +var pages = Enumerable.Return( + Enumerable.Return(1, 2, 3), + Enumerable.Return(4, 5), Enumerable.Empty(), - Sequence.Return(6)); + Enumerable.Return(6)); pages.Flatten(); // [1, 2, 3, 4, 5, 6] diff --git a/Documentation/src/enumerable-extensions/interleave.md b/Documentation/src/enumerable-extensions/interleave.md index bb7faf4d4..43e7fed50 100644 --- a/Documentation/src/enumerable-extensions/interleave.md +++ b/Documentation/src/enumerable-extensions/interleave.md @@ -23,8 +23,8 @@ For two sequences of the same length, `Interleave` is `Zip` followed by flatteni ### Example ```cs -var numbers = Sequence.Return(1, 2, 3, 4); -var letters = Sequence.Return("a", "b"); +var numbers = Enumerable.Return(1, 2, 3, 4); +var letters = Enumerable.Return("a", "b"); numbers.Select(n => n.ToString()).Interleave(letters); // ["1", "a", "2", "b", "3", "4"] @@ -33,5 +33,5 @@ numbers.Select(n => n.ToString()).Interleave(letters); Alternating rows from several sources in a fair way, for example to mix results of different providers: ```cs -var mixed = Sequence.Return(newsFeed, blogFeed, podcastFeed).Interleave().Take(30); +var mixed = Enumerable.Return(newsFeed, blogFeed, podcastFeed).Interleave().Take(30); ``` diff --git a/Documentation/src/enumerable-extensions/intersperse.md b/Documentation/src/enumerable-extensions/intersperse.md index 381f24d2e..4055d440d 100644 --- a/Documentation/src/enumerable-extensions/intersperse.md +++ b/Documentation/src/enumerable-extensions/intersperse.md @@ -23,10 +23,10 @@ list of UI elements. ### Example ```cs -Sequence.Return(9, 7, 3).Intersperse(0); +Enumerable.Return(9, 7, 3).Intersperse(0); // [9, 0, 7, 0, 3] -Sequence.Return("a").Intersperse("-"); +Enumerable.Return("a").Intersperse("-"); // ["a"] var layout = widgets.Intersperse(new Spacer()); diff --git a/Documentation/src/enumerable-extensions/join-to-string.md b/Documentation/src/enumerable-extensions/join-to-string.md index 0d1413cf1..db638930e 100644 --- a/Documentation/src/enumerable-extensions/join-to-string.md +++ b/Documentation/src/enumerable-extensions/join-to-string.md @@ -20,7 +20,7 @@ points this out. ### Example ```cs -var names = Sequence.Return("Ada", "Grace", "Linus"); +var names = Enumerable.Return("Ada", "Grace", "Linus"); names.JoinToString(", "); // "Ada, Grace, Linus" diff --git a/Documentation/src/enumerable-extensions/last-or-none.md b/Documentation/src/enumerable-extensions/last-or-none.md index 524b9f4c4..10cf5d969 100644 --- a/Documentation/src/enumerable-extensions/last-or-none.md +++ b/Documentation/src/enumerable-extensions/last-or-none.md @@ -21,7 +21,7 @@ returns on an infinite sequence. ### Example ```cs -var numbers = Sequence.Return(3, 1, 4, 1, 5); +var numbers = Enumerable.Return(3, 1, 4, 1, 5); numbers.LastOrNone(); // Some(5) numbers.LastOrNone(n => n < 3); // Some(1) diff --git a/Documentation/src/enumerable-extensions/max-or-none.md b/Documentation/src/enumerable-extensions/max-or-none.md index 8ec32e61b..d13777b34 100644 --- a/Documentation/src/enumerable-extensions/max-or-none.md +++ b/Documentation/src/enumerable-extensions/max-or-none.md @@ -22,7 +22,7 @@ first of several equal elements wins, and `None` elements are skipped. ### Example ```cs -var numbers = Sequence.Return(3, 1, 4, 1, 5); +var numbers = Enumerable.Return(3, 1, 4, 1, 5); numbers.MaxOrNone(); // Some(5) Enumerable.Empty().MaxOrNone(); // None diff --git a/Documentation/src/enumerable-extensions/memoize.md b/Documentation/src/enumerable-extensions/memoize.md index 31f45d8c6..e17fca780 100644 --- a/Documentation/src/enumerable-extensions/memoize.md +++ b/Documentation/src/enumerable-extensions/memoize.md @@ -35,7 +35,7 @@ to `GetAsyncEnumerator`, for example through `WithCancellation`, cancels that co ### Example ```cs -using var primes = Sequence.Successors(2, p => Option.Some(NextPrime(p))).Memoize(); +using var primes = Enumerable.Successors(2, p => Option.Some(NextPrime(p))).Memoize(); var firstTen = primes.Take(10).ToList(); var below100 = primes.TakeWhile(p => p < 100).ToList(); // reuses the first ten, computes the rest diff --git a/Documentation/src/enumerable-extensions/merge.md b/Documentation/src/enumerable-extensions/merge.md index c16231494..b28455bad 100644 --- a/Documentation/src/enumerable-extensions/merge.md +++ b/Documentation/src/enumerable-extensions/merge.md @@ -27,12 +27,12 @@ inputs. This is what distinguishes it from `Concat(...).OrderBy(...)`, which has ### Example ```cs -var odds = Sequence.Return(1, 3, 5, 9); -var evens = Sequence.Return(2, 4, 6); +var odds = Enumerable.Return(1, 3, 5, 9); +var evens = Enumerable.Return(2, 4, 6); odds.Merge(evens); // [1, 2, 3, 4, 5, 6, 9] var byNewest = Comparer.Create((a, b) => b.Timestamp.CompareTo(a.Timestamp)); -var combinedLog = Sequence.Return(webLog, dbLog, workerLog).Merge(byNewest); +var combinedLog = Enumerable.Return(webLog, dbLog, workerLog).Merge(byNewest); ``` diff --git a/Documentation/src/enumerable-extensions/min-by-or-none.md b/Documentation/src/enumerable-extensions/min-by-or-none.md index 99ac362b8..fd043c326 100644 --- a/Documentation/src/enumerable-extensions/min-by-or-none.md +++ b/Documentation/src/enumerable-extensions/min-by-or-none.md @@ -25,7 +25,7 @@ comparison, so keep it cheap or project the key first. ### Example ```cs -var animals = Sequence.Return( +var animals = Enumerable.Return( new Animal("Elephant", Weight: 6000), new Animal("Mouse", Weight: 0.02), new Animal("Cat", Weight: 4), diff --git a/Documentation/src/enumerable-extensions/min-or-none.md b/Documentation/src/enumerable-extensions/min-or-none.md index ab7ca1938..86b1afad9 100644 --- a/Documentation/src/enumerable-extensions/min-or-none.md +++ b/Documentation/src/enumerable-extensions/min-or-none.md @@ -26,12 +26,12 @@ same way the BCL's `Min` skips `null`. A sequence consisting only of `None` is t ### Example ```cs -var numbers = Sequence.Return(3, 1, 4, 1, 5); +var numbers = Enumerable.Return(3, 1, 4, 1, 5); numbers.MinOrNone(); // Some(1) Enumerable.Empty().MinOrNone(); // None numbers.MinOrNone(n => n * -1); // Some(-5) -var readings = Sequence.Return(Option.Some(3), Option.None, Option.Some(2)); +var readings = Enumerable.Return(Option.Some(3), Option.None, Option.Some(2)); readings.MinOrNone(); // Some(2) ``` diff --git a/Documentation/src/enumerable-extensions/pairwise.md b/Documentation/src/enumerable-extensions/pairwise.md index 29242173a..3ca679556 100644 --- a/Documentation/src/enumerable-extensions/pairwise.md +++ b/Documentation/src/enumerable-extensions/pairwise.md @@ -26,7 +26,7 @@ predecessor an `Option`, so the first element is not dropped. Differences between consecutive measurements: ```cs -var temperatures = Sequence.Return(20, 22, 21, 25); +var temperatures = Enumerable.Return(20, 22, 21, 25); temperatures.Pairwise(); // [(20, 22), (22, 21), (21, 25)] diff --git a/Documentation/src/enumerable-extensions/power-set.md b/Documentation/src/enumerable-extensions/power-set.md index 4c2434d83..903259722 100644 --- a/Documentation/src/enumerable-extensions/power-set.md +++ b/Documentation/src/enumerable-extensions/power-set.md @@ -21,7 +21,7 @@ duplicates in the input produce duplicate subsets. ### Example ```cs -Sequence.Return("a", "b", "c").PowerSet(); +Enumerable.Return("a", "b", "c").PowerSet(); // [[], [a], [b], [a, b], [c], [a, c], [b, c], [a, b, c]] ``` diff --git a/Documentation/src/enumerable-extensions/scan.md b/Documentation/src/enumerable-extensions/scan.md index 787aed601..8fa1d6842 100644 --- a/Documentation/src/enumerable-extensions/scan.md +++ b/Documentation/src/enumerable-extensions/scan.md @@ -27,7 +27,7 @@ Both produce exactly as many elements as the input and are lazy, which makes the ### Example ```cs -var amounts = Sequence.Return(10, 20, 30); +var amounts = Enumerable.Return(10, 20, 30); amounts.InclusiveScan(0, (sum, amount) => sum + amount); // [10, 30, 60] diff --git a/Documentation/src/enumerable-extensions/sequence.md b/Documentation/src/enumerable-extensions/sequence.md index 2142ad365..69219347d 100644 --- a/Documentation/src/enumerable-extensions/sequence.md +++ b/Documentation/src/enumerable-extensions/sequence.md @@ -30,10 +30,10 @@ The inverse direction, swapping a monad with a sequence inside it, lives on the ### Example ```cs -var allParsed = Sequence.Return("1", "2", "3").Select(ParseExtensions.ParseInt32OrNone).Sequence(); +var allParsed = Enumerable.Return("1", "2", "3").Select(ParseExtensions.ParseInt32OrNone).Sequence(); // Some([1, 2, 3]) -var oneBad = Sequence.Return("1", "x", "3").Select(ParseExtensions.ParseInt32OrNone).Sequence(); +var oneBad = Enumerable.Return("1", "x", "3").Select(ParseExtensions.ParseInt32OrNone).Sequence(); // None ``` diff --git a/Documentation/src/enumerable-extensions/single-or-none.md b/Documentation/src/enumerable-extensions/single-or-none.md index a2def610a..a6b749fec 100644 --- a/Documentation/src/enumerable-extensions/single-or-none.md +++ b/Documentation/src/enumerable-extensions/single-or-none.md @@ -26,11 +26,11 @@ at most two elements. ### Example ```cs -Sequence.Return(4).SingleOrNone(); // Some(4) +Enumerable.Return(4).SingleOrNone(); // Some(4) Enumerable.Empty().SingleOrNone(); // None -Sequence.Return(4, 4).SingleOrNone(); // throws InvalidOperationException +Enumerable.Return(4, 4).SingleOrNone(); // throws InvalidOperationException -var users = Sequence.Return("alice", "bob"); +var users = Enumerable.Return("alice", "bob"); users.SingleOrNone(u => u.StartsWith('a')); // Some("alice") users.SingleOrNone(u => u.StartsWith('z')); // None ``` diff --git a/Documentation/src/enumerable-extensions/sliding-window.md b/Documentation/src/enumerable-extensions/sliding-window.md index ad16de78f..19a46e1ea 100644 --- a/Documentation/src/enumerable-extensions/sliding-window.md +++ b/Documentation/src/enumerable-extensions/sliding-window.md @@ -25,7 +25,7 @@ produced lazily and each one is an immutable snapshot. A moving average over the last three readings: ```cs -var readings = Sequence.Return(2, 4, 6, 8, 10); +var readings = Enumerable.Return(2, 4, 6, 8, 10); readings.SlidingWindow(3); // [[2, 4, 6], [4, 6, 8], [6, 8, 10]] diff --git a/Documentation/src/enumerable-extensions/split.md b/Documentation/src/enumerable-extensions/split.md index ef8a7c001..5e9386958 100644 --- a/Documentation/src/enumerable-extensions/split.md +++ b/Documentation/src/enumerable-extensions/split.md @@ -26,7 +26,7 @@ restores the source. The parts are produced lazily and each part is materialized Splitting a token stream into statements: ```cs -var tokens = Sequence.Return("let", "x", ";", "print", "x", ";", ";", "exit"); +var tokens = Enumerable.Return("let", "x", ";", "print", "x", ";", ";", "exit"); tokens.Split(";"); // [["let", "x"], ["print", "x"], [], ["exit"]] diff --git a/Documentation/src/enumerable-extensions/transpose.md b/Documentation/src/enumerable-extensions/transpose.md index f41be071e..c7f38a4ca 100644 --- a/Documentation/src/enumerable-extensions/transpose.md +++ b/Documentation/src/enumerable-extensions/transpose.md @@ -21,9 +21,9 @@ sequence transposes to an empty sequence. ### Example ```cs -var matrix = Sequence.Return( - Sequence.Return(1, 2, 3), - Sequence.Return(4, 5, 6)); +var matrix = Enumerable.Return( + Enumerable.Return(1, 2, 3), + Enumerable.Return(4, 5, 6)); matrix.Transpose(); // [[1, 4], [2, 5], [3, 6]] diff --git a/Documentation/src/enumerable-extensions/traverse.md b/Documentation/src/enumerable-extensions/traverse.md index 68fd89031..c419715b6 100644 --- a/Documentation/src/enumerable-extensions/traverse.md +++ b/Documentation/src/enumerable-extensions/traverse.md @@ -31,8 +31,8 @@ is done with the results: Option> ParseAll(IEnumerable input) => input.Traverse(ParseExtensions.ParseInt32OrNone); -ParseAll(Sequence.Return("1", "2", "3")); // Some([1, 2, 3]) -ParseAll(Sequence.Return("1", "x", "3")); // None +ParseAll(Enumerable.Return("1", "2", "3")); // Some([1, 2, 3]) +ParseAll(Enumerable.Return("1", "x", "3")); // None ``` ```cs diff --git a/Documentation/src/enumerable-extensions/where-not-null.md b/Documentation/src/enumerable-extensions/where-not-null.md index 7a1046fea..b34f27b27 100644 --- a/Documentation/src/enumerable-extensions/where-not-null.md +++ b/Documentation/src/enumerable-extensions/where-not-null.md @@ -24,11 +24,11 @@ before `FirstOrNone` and friends on a sequence with nullable elements. ### Example ```cs -IEnumerable lines = Sequence.Return("a", null, "b"); +IEnumerable lines = Enumerable.Return("a", null, "b"); IEnumerable nonNull = lines.WhereNotNull(); // ["a", "b"] -IEnumerable readings = Sequence.Return(1, null, 3); +IEnumerable readings = Enumerable.Return(1, null, 3); IEnumerable values = readings.WhereNotNull(); // [1, 3] diff --git a/Documentation/src/enumerable-extensions/where-select.md b/Documentation/src/enumerable-extensions/where-select.md index 35fdce518..ea5b24a95 100644 --- a/Documentation/src/enumerable-extensions/where-select.md +++ b/Documentation/src/enumerable-extensions/where-select.md @@ -30,7 +30,7 @@ hands the selector the element's position as well, like the indexed `Select`. Parsing what can be parsed: ```cs -var input = Sequence.Return("12", "abc", "7", "", "40"); +var input = Enumerable.Return("12", "abc", "7", "", "40"); input.WhereSelect(ParseExtensions.ParseInt32OrNone); // [12, 7, 40] @@ -46,6 +46,6 @@ IEnumerable FindUsers(IEnumerable ids) Keeping only the present values of a sequence of options: ```cs -var found = Sequence.Return(Option.Some(1), Option.None, Option.Some(3)).WhereSelect(); +var found = Enumerable.Return(Option.Some(1), Option.None, Option.Some(3)).WhereSelect(); // [1, 3] ``` diff --git a/Documentation/src/enumerable-extensions/with-index.md b/Documentation/src/enumerable-extensions/with-index.md index b99ca73ed..7dbec670a 100644 --- a/Documentation/src/enumerable-extensions/with-index.md +++ b/Documentation/src/enumerable-extensions/with-index.md @@ -23,7 +23,7 @@ target framework and puts the value first, matching the other `With…` types. ### Example ```cs -var names = Sequence.Return("Ada", "Grace", "Linus"); +var names = Enumerable.Return("Ada", "Grace", "Linus"); names.WithIndex().Select(n => $"{n.Index + 1}. {n.Value}"); // ["1. Ada", "2. Grace", "3. Linus"] diff --git a/Documentation/src/enumerable-extensions/with-previous.md b/Documentation/src/enumerable-extensions/with-previous.md index ec68a8353..752b8ce31 100644 --- a/Documentation/src/enumerable-extensions/with-previous.md +++ b/Documentation/src/enumerable-extensions/with-previous.md @@ -25,7 +25,7 @@ The element type is constrained to `notnull`, because `Previous` is an `Option pair.Match( both: (e, a) => e == a ? $"{e} ok" : $"{e} != {a}", diff --git a/Documentation/src/functional-helpers/compose.md b/Documentation/src/functional-helpers/compose.md index 0fcb86701..6b646a5e8 100644 --- a/Documentation/src/functional-helpers/compose.md +++ b/Documentation/src/functional-helpers/compose.md @@ -21,7 +21,7 @@ Func> parse = ParseExtensions.ParseInt32OrNone; var parseLoosely = parse.Compose(toLower).Compose(trim); // trim, then lower, then parse -Sequence.Return(" 42 ", "x").WhereSelect(parseLoosely); // [42] +Enumerable.Return(" 42 ", "x").WhereSelect(parseLoosely); // [42] ``` Within a LINQ query you rarely need `Compose`, because `Select(g).Select(f)` composes for you. It is for the diff --git a/Documentation/src/sequence/sequence.md b/Documentation/src/sequence/sequence.md index 1635962a6..774f4725e 100644 --- a/Documentation/src/sequence/sequence.md +++ b/Documentation/src/sequence/sequence.md @@ -2,15 +2,9 @@ LINQ is good at transforming sequences and poor at creating them. `Enumerable.Range` and `Enumerable.Repeat` are the only constructors it offers, so code that needs a one-element sequence ends up with `new[] { x }`, -and code that needs an unfolding sequence ends up with a `while` loop and `yield`. The static class -`Funcky.Sequence` fills that gap. - -All members are static methods on `Sequence`, so they are called as `Sequence.Return(...)` and so on. Those -that take a function or a lazy input are lazy themselves; `Return` and `FromNullable` are small materialized -lists. - -Since Funcky 3.8.0 the same constructors are also C# 14 static extension members of `System.Linq.Enumerable`, so -they sit next to `Enumerable.Range`, `Enumerable.Repeat` and `Enumerable.Empty` where LINQ users look for them: +and code that needs an unfolding sequence ends up with a `while` loop and `yield`. Funcky fills that gap with +static extension members of `System.Linq.Enumerable`, so the constructors sit next to `Enumerable.Range`, +`Enumerable.Repeat` and `Enumerable.Empty` where LINQ users look for them: ```cs var naturals = Enumerable.Successors(1, n => n + 1); @@ -18,10 +12,14 @@ var single = Enumerable.Return(42); using var cycle = Enumerable.CycleRange(new[] { 1, 2, 3 }); ``` -`Enumerable.Concat(a, b)` with exactly two sequences binds to the BCL method, which does the same; with one +Those that take a function or a lazy input are lazy themselves; `Return` and `FromNullable` are small materialized +lists. `Enumerable.Concat(a, b)` with exactly two sequences binds to the BCL method, which does the same; with one sequence of sequences or three or more sequences it binds to Funcky's. The members are declared in -`Funcky.Extensions.EnumerableExtensions`; on a compiler older than C# 14 they are plain static methods of that class, -and `Sequence` remains the form to use. +`Funcky.Extensions.EnumerableExtensions`, so `using Funcky.Extensions;` brings them into scope. + +Up to Funcky 3.8.0 the same constructors were static methods of the class `Funcky.Sequence`, `Sequence.Return(42)`. +Funcky 4.0.0 removed that class; on 3.8.0 the analyzer rule λ1202 rewrites every call to the `Enumerable` form, +which is the upgrade path. | Constructor | Produces | |----------------------------------------------------|-----------------------------------------------------------------| @@ -43,7 +41,7 @@ IReadOnlyList Return(params TResult[] elements) ``` `Return` is the name monads use for "put a plain value into the monad", and `IEnumerable` is a monad. Where -you would write `Option.Return(x)` or `Result.Return(x)`, you write `Sequence.Return(x)` for a sequence. It +you would write `Option.Return(x)` or `Result.Return(x)`, you write `Enumerable.Return(x)` for a sequence. It replaces `new[] { x }`, `new List { x }` and `Enumerable.Repeat(x, 1)`; the analyzer [λ1001](../analyzer-rules/λ1001.md) flags the last one. @@ -53,17 +51,17 @@ of times. It is a read-only view over the array the compiler builds for the call view cannot be cast back to a mutable array. If you pass an existing array yourself, the view reflects later changes to that array. -Passing an array binds to the `params` overload, as with any `params` method: `Sequence.Return(array)` is a +Passing an array binds to the `params` overload, as with any `params` method: `Enumerable.Return(array)` is a sequence of the array's elements. To get a sequence whose single element is the array, name the type argument, -`Sequence.Return(array)`. +`Enumerable.Return(array)`. ```cs -IEnumerable greeting = Sequence.Return("hello"); +IEnumerable greeting = Enumerable.Return("hello"); -var suits = Sequence.Return("♠", "♣", "♥", "♦"); +var suits = Enumerable.Return("♠", "♣", "♥", "♦"); // Prepend a header to a lazy sequence without materializing it -var report = Sequence.Return(header).Concat(rows); +var report = Enumerable.Return(header).Concat(rows); ``` ## FromNullable @@ -79,7 +77,7 @@ This is `Option.FromNullable` followed by `ToEnumerable`, in one step. It is use simply contribute nothing, for example inside a `SelectMany`: ```cs -var inner = exceptions.SelectMany(e => Sequence.FromNullable(e.InnerException)); +var inner = exceptions.SelectMany(e => Enumerable.FromNullable(e.InnerException)); ``` ## Successors @@ -111,22 +109,22 @@ The first element is always included. `Skip(1)` if you only want what follows it ```cs // All non-negative integers -Sequence.Successors(0, n => n + 1); +Enumerable.Successors(0, n => n + 1); // Powers of two that fit in an int -Sequence.Successors(1, n => n <= int.MaxValue / 2 ? Option.Some(n * 2) : Option.None); +Enumerable.Successors(1, n => n <= int.MaxValue / 2 ? Option.Some(n * 2) : Option.None); // The chain of inner exceptions -Sequence.Successors(exception, e => Option.FromNullable(e.InnerException)); +Enumerable.Successors(exception, e => Option.FromNullable(e.InnerException)); // Walking up a directory tree -Sequence.Successors(directory, d => Option.FromNullable(d.Parent)); +Enumerable.Successors(directory, d => Option.FromNullable(d.Parent)); // Fibonacci, by carrying a pair of state and projecting it away -Sequence.Successors((0, 1), p => (p.Item2, p.Item1 + p.Item2)).Select(p => p.Item1); +Enumerable.Successors((0, 1), p => (p.Item2, p.Item1 + p.Item2)).Select(p => p.Item1); // All days of a year, see the calendar case study -Sequence.Successors(JanuaryFirst(year), NextDay).TakeWhile(day => day.Year == year); +Enumerable.Successors(JanuaryFirst(year), NextDay).TakeWhile(day => day.Year == year); ``` The last example is a pattern worth recognising: anything you would express with a `for` loop whose state is @@ -144,7 +142,7 @@ This is `Enumerable.Repeat` without a count, or `Successors(element, Identity)`. a constant against another sequence, or a placeholder stream to be cut with `Take`. ```cs -var padded = words.ZipLongest(Sequence.Cycle("").Take(10), pair => pair.Match( +var padded = words.ZipLongest(Enumerable.Cycle("").Take(10), pair => pair.Match( left: w => w, right: p => p, both: (w, _) => w)); ``` @@ -176,11 +174,11 @@ can neither end nor produce anything. For `CycleRange` this happens on enumerati read to its end. ```cs -var colors = Sequence.Return("red", "green", "blue"); +var colors = Enumerable.Return("red", "green", "blue"); -var striped = rows.Zip(Sequence.CycleMaterialized(colors), (row, color) => (row, color)); +var striped = rows.Zip(Enumerable.CycleMaterialized(colors), (row, color) => (row, color)); -using var schedule = Sequence.CycleRange(ReadShiftsFromFile()); +using var schedule = Enumerable.CycleRange(ReadShiftsFromFile()); var nextTen = schedule.Take(10).ToList(); ``` @@ -199,10 +197,10 @@ repeatedly. Unlike the cycle variants, an empty source is not an error: repeatin A count of zero yields an empty sequence. ```cs -Sequence.RepeatMaterialized(Sequence.Return(1, 2), 3); +Enumerable.RepeatMaterialized(Enumerable.Return(1, 2), 3); // [1, 2, 1, 2, 1, 2] -var border = Sequence.RepeatMaterialized(Sequence.Return('-'), width).ConcatToString(); +var border = Enumerable.RepeatMaterialized(Enumerable.Return('-'), width).ConcatToString(); ``` ## Concat @@ -221,12 +219,12 @@ IEnumerable Concat(IEnumerable> sources) -LINQ's `Concat` takes exactly two sequences, so joining three reads `a.Concat(b).Concat(c)`. `Sequence.Concat(a, b, c)` +LINQ's `Concat` takes exactly two sequences, so joining three reads `a.Concat(b).Concat(c)`. `Enumerable.Concat(a, b, c)` says the same thing once. It is lazy, enumerates the inputs in order, and is the same operation as [`Flatten`](../enumerable-extensions/combining-sequences.md#flatten) on the sequence of sequences. ```cs -var all = Sequence.Concat(header, body, footer); +var all = Enumerable.Concat(header, body, footer); ``` ## Asynchronous counterparts diff --git a/Documentation/src/stream-extensions.md b/Documentation/src/stream-extensions.md index c77b50db0..cb74fb10d 100644 --- a/Documentation/src/stream-extensions.md +++ b/Documentation/src/stream-extensions.md @@ -42,7 +42,7 @@ string DescribeProgress(Stream stream) ```cs IEnumerable Bytes(Stream stream) - => Sequence.Successors(stream.ReadByteOrNone(), _ => stream.ReadByteOrNone()); + => Enumerable.Successors(stream.ReadByteOrNone(), _ => stream.ReadByteOrNone()); ``` See [The TryVerb-pattern](./try-pattern.md) for the same idea across the rest of the BCL. diff --git a/Documentation/src/xunit.md b/Documentation/src/xunit.md index 4f51f2f91..a167810f0 100644 --- a/Documentation/src/xunit.md +++ b/Documentation/src/xunit.md @@ -23,10 +23,10 @@ using Funcky.Extensions; Assert.Some(42, FindUser(id).Select(user => user.Age)); ``` -Extension members need the C# 14 compiler (.NET 10 SDK). Older compilers use the same assertions as methods of -the `Funcky.FunctionalAssert` class, `FunctionalAssert.Some(42, option)`. The two forms are the same code; the -`Assert` members are the implementation and `FunctionalAssert` forwards to them. If both are available, -[λ1203](./analyzer-rules/λ1203.md) asks for the `Assert` form and comes with a code fix. +Extension members need the C# 14 compiler, which the .NET 10 SDK that Funcky 4 requires provides. Up to Funcky.Xunit +2.x and Funcky.Xunit.v3 1.x the same assertions were also methods of a `Funcky.FunctionalAssert` class, +`FunctionalAssert.Some(42, option)`; Funcky.Xunit 3.0.0 and Funcky.Xunit.v3 2.0.0 removed that class. On the last +2.x release the analyzer rule λ1203 rewrites every call to the `Assert` form, which is the upgrade path. ### xUnit.net 3.x versions @@ -136,26 +136,26 @@ A failed assertion throws an `XunitException`, so the test runner shows it like message names the assertion and prints both sides in the notation the monads use in `ToString`: ```text -FunctionalAssert.Some() Failure: Values differ +Assert.Some() Failure: Values differ Expected: Some(10) Actual: None ``` ```text -FunctionalAssert.Ok() Failure: Values differ +Assert.Ok() Failure: Values differ Expected: Ok(10) Actual: Error(System.InvalidOperationException: "boom") ``` ```text -FunctionalAssert.Error() Failure: Values differ +Assert.Error() Failure: Values differ Expected: Error(ArgumentException) Actual: Error(InvalidOperationException) ``` The returning overloads cannot know the expected content and print `Some(...)`, `Ok(...)`, `Error(...)`, -`Left(...)` or `Right(...)` as the expected side. The label says `FunctionalAssert` in both forms, so a message -looks the same whichever receiver the test used. +`Left(...)` or `Right(...)` as the expected side. The label matches xunit's own `Assert.Equal() Failure`; up to +Funcky.Xunit 2.x it read `FunctionalAssert.Some()`, so a test that asserts on the message text changes with 3.0.0. On .NET 6 and later the assertion's own frames are hidden from the stack trace with `[StackTraceHidden]`, so the first line of the trace is the test that failed. On older frameworks a rethrow erases the internal frames of the @@ -226,4 +226,4 @@ The serializers register themselves. The package's build targets add the three a * [Option Monad](./option.md), [Result Monad](./result.md) and [Either Monad](./either.md) describe the types the assertions unwrap. -* [λ1101](./analyzer-rules/λ1101.md) and [λ1203](./analyzer-rules/λ1203.md) are the two analyzer rules for test code. +* [λ1101](./analyzer-rules/λ1101.md) is the analyzer rule for test code. diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.Designer.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.Designer.cs index aa0a162be..23241d2e5 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.Designer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.Designer.cs @@ -79,7 +79,7 @@ internal static string EnumerableRepeatNeverCodeFixTitle { } /// - /// Looks up a localized string similar to Use Sequence.Return. + /// Looks up a localized string similar to Use Enumerable.Return. /// internal static string EnumerableRepeatOnceCodeFixTitle { get { diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.resx b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.resx index eeee531c3..8bf0a177c 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.resx +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeFixResources.resx @@ -126,7 +126,7 @@ The title of the code fix. - Use Sequence.Return + Use Enumerable.Return The title of the code fix. diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs index 300e8dbc7..7ebae8348 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs @@ -34,49 +34,50 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration && diagnostic.TryGetIntProperty(ValueParameterIndexProperty, out var valueParameterIndex)) { - context.RegisterCodeFix(new ToSequenceReturnCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); + context.RegisterCodeFix(new ToEnumerableReturnCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private sealed class ToSequenceReturnCodeAction : CodeAction + private sealed class ToEnumerableReturnCodeAction : CodeAction { private readonly Document _document; private readonly InvocationExpressionSyntax _invocationExpression; private readonly int _valueParameterIndex; - public ToSequenceReturnCodeAction(Document document, InvocationExpressionSyntax invocationExpression, int valueParameterIndex) + public ToEnumerableReturnCodeAction(Document document, InvocationExpressionSyntax invocationExpression, int valueParameterIndex) { _document = document; _invocationExpression = invocationExpression; _valueParameterIndex = valueParameterIndex; } - public override string Title => EnumerableRepeatNeverCodeFixTitle; + public override string Title => EnumerableRepeatOnceCodeFixTitle; - public override string EquivalenceKey => nameof(ToSequenceReturnCodeAction); + public override string EquivalenceKey => nameof(ToEnumerableReturnCodeAction); protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) { var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); var valueArgument = _invocationExpression.ArgumentList.Arguments[_valueParameterIndex]; - editor.ReplaceNode(_invocationExpression, CreateSequenceReturnRoot(valueArgument, editor.SemanticModel, editor.Generator)); + editor.ReplaceNode(_invocationExpression, CreateEnumerableReturnRoot(valueArgument, editor.SemanticModel, editor.Generator)); return editor.GetChangedDocument(); } - private static SyntaxNode CreateSequenceReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) - => SyntaxSequenceReturn(model, generator) + private static SyntaxNode CreateEnumerableReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) + => SyntaxEnumerableReturn(model, generator) .WithArgumentList(ArgumentList(SingletonSeparatedList(firstArgument.WithNameColon(null))) .WithCloseParenToken(Token(SyntaxKind.CloseParenToken))) .NormalizeWhitespace(); - private static InvocationExpressionSyntax SyntaxSequenceReturn(SemanticModel model, SyntaxGenerator generator) + // Enumerable.Return is Funcky's static extension member on System.Linq.Enumerable, so the receiver is the BCL type. + private static InvocationExpressionSyntax SyntaxEnumerableReturn(SemanticModel model, SyntaxGenerator generator) => InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetSequenceType()!), + (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetEnumerableType()!), IdentifierName(MonadReturnMethodName)) .WithAdditionalAnnotations(Simplifier.Annotation)); } diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferEnumerableSequenceConstructorsCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferEnumerableSequenceConstructorsCodeFix.cs deleted file mode 100644 index 0bc2ba178..000000000 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferEnumerableSequenceConstructorsCodeFix.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Immutable; -using System.Composition; -using Funcky.Analyzers.CodeAnalysisExtensions; -using Funcky.Analyzers.ExtensionForms; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Simplification; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Funcky.Analyzers; - -[Shared] -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferEnumerableSequenceConstructorsCodeFix))] -public sealed class PreferEnumerableSequenceConstructorsCodeFix : ExtensionMemberCodeFix -{ - private const string EnumerableTypeName = "global::System.Linq.Enumerable"; - - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(PreferEnumerableSequenceConstructorsAnalyzer.PreferEnumerable.Id); - - protected override string GetTitle(ExtensionMemberRewrite rewrite) => "Replace with the Enumerable form"; - - protected override ExtensionMemberRewrite? TryCreateRewrite(SyntaxNode root, SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken) - => root.FindInvocationExpression(diagnostic.Location.SourceSpan) is { } invocation - && invocation.Span == diagnostic.Location.SourceSpan - && WithEnumerableReceiver(invocation.Expression) is { } replacement - ? new ExtensionMemberRewrite(invocation, replacement) - : null; - - // `Sequence.Return(x)` and `Funcky.Sequence.Return(x)` keep their name (and type arguments); `Return(x)` through a - // `using static` gets the Enumerable receiver in front. - private static InvocationExpressionSyntax? WithEnumerableReceiver(ExpressionSyntax invoked) - => invoked switch - { - MemberAccessExpressionSyntax memberAccess => ((InvocationExpressionSyntax)invoked.Parent!).WithExpression(memberAccess.WithExpression(Enumerable().WithTriviaFrom(memberAccess.Expression))), - SimpleNameSyntax name => ((InvocationExpressionSyntax)invoked.Parent!).WithExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, Enumerable().WithLeadingTrivia(name.GetLeadingTrivia()), name.WithoutLeadingTrivia())), - _ => null, - }; - - private static ExpressionSyntax Enumerable() - => ParseExpression(EnumerableTypeName).WithAdditionalAnnotations(Simplifier.Annotation); -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferXunitAssertCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferXunitAssertCodeFix.cs deleted file mode 100644 index 51410a5d6..000000000 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/ExtensionForms/PreferXunitAssertCodeFix.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Immutable; -using System.Composition; -using Funcky.Analyzers.CodeAnalysisExtensions; -using Funcky.Analyzers.ExtensionForms; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Simplification; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Funcky.Analyzers; - -[Shared] -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferXunitAssertCodeFix))] -public sealed class PreferXunitAssertCodeFix : ExtensionMemberCodeFix -{ - private const string AssertTypeName = "global::Xunit.Assert"; - - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(PreferXunitAssertAnalyzer.PreferAssert.Id); - - protected override string GetTitle(ExtensionMemberRewrite rewrite) => "Replace with the assertion on Assert"; - - protected override ExtensionMemberRewrite? TryCreateRewrite(SyntaxNode root, SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken) - => root.FindInvocationExpression(diagnostic.Location.SourceSpan) is { } invocation - && invocation.Span == diagnostic.Location.SourceSpan - && WithAssertReceiver(invocation) is { } replacement - ? new ExtensionMemberRewrite(invocation, replacement) - : null; - - // `FunctionalAssert.Some(x)` and `Funcky.FunctionalAssert.Some(x)` keep their name (and type arguments); `Some(x)` through - // a `using static` gets the Assert receiver in front. - private static InvocationExpressionSyntax? WithAssertReceiver(InvocationExpressionSyntax invocation) - => invocation.Expression switch - { - MemberAccessExpressionSyntax memberAccess => invocation.WithExpression(memberAccess.WithExpression(Assert().WithTriviaFrom(memberAccess.Expression))), - SimpleNameSyntax name => invocation.WithExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, Assert().WithLeadingTrivia(name.GetLeadingTrivia()), name.WithoutLeadingTrivia())), - _ => null, - }; - - private static ExpressionSyntax Assert() - => ParseExpression(AssertTypeName).WithAdditionalAnnotations(Simplifier.Annotation); -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs index fb17183d1..7aff2ac2d 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs @@ -15,7 +15,7 @@ public async Task EnumerableRepeatWithAnyNumberButOneIssuesNoDiagnostic() } [Fact] - public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnostic() + public async Task UsingEnumerableRepeatOnceShowsTheEnumerableReturnDiagnostic() { var expectedDiagnostic = VerifyCS .Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId) @@ -26,7 +26,7 @@ public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnostic() } [Fact] - public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnosticWhenArgumentsAreFlipped() + public async Task UsingEnumerableRepeatOnceShowsTheEnumerableReturnDiagnosticWhenArgumentsAreFlipped() { var expectedDiagnostic = VerifyCS .Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId) @@ -37,14 +37,14 @@ public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnosticWhenA } [Fact] - public async Task UsingEnumerableRepeatOnceShowsNoDiagnosticWhenSequenceTypeIsNotAvailable() + public async Task UsingEnumerableRepeatOnceShowsNoDiagnosticWhenEnumerableExtensionsAreNotAvailable() { - var inputCode = File.ReadAllText("TestCode/RepeatOnceMissingSequenceType.input"); + var inputCode = File.ReadAllText("TestCode/RepeatOnceMissingEnumerableExtensions.input"); await VerifyCS.VerifyAnalyzerAsync(inputCode); } [Fact] - public async Task UsingEnumerableRepeatOnceViaConstantShowsTheSequenceReturnDiagnostic() + public async Task UsingEnumerableRepeatOnceViaConstantShowsTheEnumerableReturnDiagnostic() { var expectedDiagnostic = VerifyCS .Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId) @@ -59,9 +59,9 @@ public async Task CodeFixWorksWithDifferentUsingStyles() { DiagnosticResult[] expectedDiagnostics = [ - VerifyCS.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId).WithSpan(17, 17, 17, 53).WithArguments("10"), - VerifyCS.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId).WithSpan(28, 17, 28, 53).WithArguments("10"), - VerifyCS.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId).WithSpan(41, 17, 41, 53).WithArguments("10"), + VerifyCS.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId).WithSpan(21, 17, 21, 41).WithArguments("10"), + VerifyCS.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId).WithSpan(34, 17, 34, 53).WithArguments("10"), + VerifyCS.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId).WithSpan(48, 17, 48, 34).WithArguments("10"), ]; await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostics, "RepeatOnceQualification"); diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/PreferEnumerableSequenceConstructorsTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/PreferEnumerableSequenceConstructorsTest.cs deleted file mode 100644 index 7fcaa26ba..000000000 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/PreferEnumerableSequenceConstructorsTest.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Xunit; -using VerifyCS = Funcky.Analyzers.Test.CSharpCodeFixVerifier; - -namespace Funcky.Analyzers.Test; - -public sealed class PreferEnumerableSequenceConstructorsTest -{ - // language=csharp - private const string StubCode = - """ - namespace Funcky - { - using System; - using System.Collections.Generic; - - public static class Sequence - { - public static IReadOnlyList Return(TResult element) => new[] { element }; - - public static IReadOnlyList Return(params TResult[] elements) => elements; - - public static IEnumerable Successors(TResult first, Func successor) => new[] { first }; - - public static IEnumerable Generate(Func generator) => new[] { generator() }; - } - } - - namespace Funcky.Extensions - { - using System; - using System.Collections.Generic; - using System.Linq; - - public static class EnumerableExtensions - { - extension(Enumerable) - { - public static IReadOnlyList Return(TResult element) => new[] { element }; - - public static IReadOnlyList Return(params TResult[] elements) => elements; - - public static IEnumerable Successors(TResult first, Func successor) => new[] { first }; - } - - extension(IEnumerable source) - { - public IEnumerable Generate() => source; - } - } - } - """; - - [Fact] - public async Task ReportsTheSequenceConstructorsAndRewritesThemToEnumerable() - { - const string inputCode = - """ - using System.Collections.Generic; - using System.Linq; - using Funcky; - using Funcky.Extensions; - using static Funcky.Sequence; - - public static class C - { - public static void M() - { - IReadOnlyList a = {|λ1202:Sequence.Return(1)|}; - IReadOnlyList b = {|λ1202:Sequence.Return(1, 2)|}; - IEnumerable c = {|λ1202:Funcky.Sequence.Successors(1, n => n + 1)|}; - IReadOnlyList d = {|λ1202:Return(1)|}; - IEnumerable e = Sequence.Generate(() => 1); - } - } - """; - const string fixedCode = - """ - using System.Collections.Generic; - using System.Linq; - using Funcky; - using Funcky.Extensions; - using static Funcky.Sequence; - - public static class C - { - public static void M() - { - IReadOnlyList a = Enumerable.Return(1); - IReadOnlyList b = Enumerable.Return(1, 2); - IEnumerable c = Enumerable.Successors(1, n => n + 1); - IReadOnlyList d = Enumerable.Return(1); - IEnumerable e = Sequence.Generate(() => 1); - } - } - """; - - await VerifyCS.VerifyCodeFixAsync(inputCode + Environment.NewLine + StubCode, fixedCode + Environment.NewLine + StubCode); - } - - // Three or more diagnostics: Fix-All has to rewrite every call in one pass although each fix on its own adds the same using (#1009). - [Fact] - public async Task AddsTheUsingForTheExtensionWhenMissing() - { - const string inputCode = - """ - using System.Collections.Generic; - using Funcky; - - public static class C - { - public static IReadOnlyList Single() => {|λ1202:Sequence.Return(1)|}; - - public static IReadOnlyList Many() => {|λ1202:Sequence.Return(1, 2)|}; - - public static IEnumerable Successors() => {|λ1202:Funcky.Sequence.Successors(1, n => n + 1)|}; - } - """; - const string fixedCode = - """ - using System.Collections.Generic; - using Funcky; - using Funcky.Extensions; - - public static class C - { - public static IReadOnlyList Single() => System.Linq.Enumerable.Return(1); - - public static IReadOnlyList Many() => System.Linq.Enumerable.Return(1, 2); - - public static IEnumerable Successors() => System.Linq.Enumerable.Successors(1, n => n + 1); - } - """; - - await VerifyCS.VerifyCodeFixAsync(inputCode + Environment.NewLine + StubCode, fixedCode + Environment.NewLine + StubCode); - } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/PreferStaticParseOrNoneTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/PreferStaticParseOrNoneTest.cs index 4e5d3cb03..f8bdc2ae7 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/PreferStaticParseOrNoneTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/PreferStaticParseOrNoneTest.cs @@ -1,4 +1,3 @@ -using Microsoft.CodeAnalysis.CSharp; using Xunit; using VerifyCS = Funcky.Analyzers.Test.CSharpCodeFixVerifier; @@ -367,47 +366,4 @@ public static void M(string text) await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + StubCode); } - - [Fact] - public async Task DoesNotReportBelowCSharp14() - { - const string inputCode = - """ - using Funcky.Extensions; - using Funcky.Monads; - - public static class C - { - public static void M(string text) - { - Option a = text.ParseInt32OrNone(); - } - } - - namespace Funcky.Monads - { - public readonly struct Option - where TItem : notnull - { - } - } - - namespace Funcky.Extensions - { - using Funcky.Monads; - - public static class ParseExtensions - { - public static Option ParseInt32OrNone(this string candidate) => default; - } - - public static class Int32ParseExtensions - { - public static Option ParseOrNone(string candidate) => default; - } - } - """; - - await new VerifyCS.Test { TestCode = inputCode, LanguageVersion = LanguageVersion.CSharp13 }.RunAsync(CancellationToken.None); - } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/PreferXunitAssertTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/PreferXunitAssertTest.cs deleted file mode 100644 index 1a8ee8bb8..000000000 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/PreferXunitAssertTest.cs +++ /dev/null @@ -1,198 +0,0 @@ -using Xunit; -using VerifyCS = Funcky.Analyzers.Test.CSharpCodeFixVerifier; - -namespace Funcky.Analyzers.Test; - -public sealed class PreferXunitAssertTest -{ - // language=csharp - private const string StubCode = - """ - namespace Xunit - { - public partial class Assert - { - public static void Equal(T expected, T actual) - { - } - } - } - - namespace Funcky.Monads - { - public readonly struct Option - where TItem : notnull - { - } - } - - namespace Funcky - { - using Funcky.Monads; - - public static class FunctionalAssert - { - public static void Some(TItem expectedValue, Option option) where TItem : notnull { } - - public static TItem Some(Option option) where TItem : notnull => default!; - - public static void None(Option option) where TItem : notnull { } - - public static void Legacy(Option option) where TItem : notnull { } - } - } - - namespace Funcky.Extensions - { - using Funcky.Monads; - using Xunit; - - public static class AssertExtensions - { - extension(Assert) - { - public static void Some(TItem expectedValue, Option option) where TItem : notnull { } - - public static TItem Some(Option option) where TItem : notnull => default!; - - public static void None(Option option) where TItem : notnull { } - } - } - } - """; - - [Fact] - public async Task ReportsFunctionalAssertAndRewritesItToAssert() - { - const string inputCode = - """ - using Funcky; - using Funcky.Extensions; - using Funcky.Monads; - using Xunit; - using static Funcky.FunctionalAssert; - - public static class C - { - public static void M(Option option) - { - {|λ1203:FunctionalAssert.Some(10, option)|}; - var value = {|λ1203:FunctionalAssert.Some(option)|}; - {|λ1203:Funcky.FunctionalAssert.None(option)|}; - {|λ1203:None(option)|}; - FunctionalAssert.Legacy(option); - Assert.Equal(10, value); - } - } - """; - const string fixedCode = - """ - using Funcky; - using Funcky.Extensions; - using Funcky.Monads; - using Xunit; - using static Funcky.FunctionalAssert; - - public static class C - { - public static void M(Option option) - { - Assert.Some(10, option); - var value = Assert.Some(option); - Assert.None(option); - Assert.None(option); - FunctionalAssert.Legacy(option); - Assert.Equal(10, value); - } - } - """; - - await VerifyCS.VerifyCodeFixAsync(inputCode + Environment.NewLine + StubCode, fixedCode + Environment.NewLine + StubCode); - } - - // Three or more diagnostics: Fix-All has to rewrite every call in one pass although each fix on its own adds the same using (#1009). - [Fact] - public async Task AddsTheUsingForTheExtensionWhenMissing() - { - const string inputCode = - """ - using Funcky; - using Funcky.Monads; - using Xunit; - - public static class C - { - public static void M(Option option) - { - {|λ1203:FunctionalAssert.None(option)|}; - {|λ1203:FunctionalAssert.Some(10, option)|}; - var value = {|λ1203:FunctionalAssert.Some(option)|}; - Assert.Equal(10, value); - } - } - """; - const string fixedCode = - """ - using Funcky; - using Funcky.Monads; - using Xunit; - using Funcky.Extensions; - - public static class C - { - public static void M(Option option) - { - Assert.None(option); - Assert.Some(10, option); - var value = Assert.Some(option); - Assert.Equal(10, value); - } - } - """; - - await VerifyCS.VerifyCodeFixAsync(inputCode + Environment.NewLine + StubCode, fixedCode + Environment.NewLine + StubCode); - } - - [Fact] - public async Task DoesNotReportInsideExpressionTrees() - { - const string inputCode = - """ - using System; - using System.Linq.Expressions; - using Funcky; - using Funcky.Extensions; - using Funcky.Monads; - using Xunit; - - public static class C - { - public static void M(Option option) - { - Expression, int>> e = option => FunctionalAssert.Some(option); - Func, int> f = option => {|λ1203:FunctionalAssert.Some(option)|}; - } - } - """; - const string fixedCode = - """ - using System; - using System.Linq.Expressions; - using Funcky; - using Funcky.Extensions; - using Funcky.Monads; - using Xunit; - - public static class C - { - public static void M(Option option) - { - Expression, int>> e = option => FunctionalAssert.Some(option); - Func, int> f = option => Assert.Some(option); - } - } - """; - - await VerifyCS.VerifyCodeFixAsync(inputCode + Environment.NewLine + StubCode, fixedCode + Environment.NewLine + StubCode); - } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/SequenceReturnCollectionExpressionTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/SequenceReturnCollectionExpressionTest.cs index 2d673de48..a64763ec5 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/SequenceReturnCollectionExpressionTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/SequenceReturnCollectionExpressionTest.cs @@ -9,19 +9,19 @@ public sealed class SequenceReturnCollectionExpressionTest [Fact] public async Task SuggestsCollectionExpressionForExplicitlyTypedLocal() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { IReadOnlyList M() { - IReadOnlyList result = {|λ1013:Sequence.Return(0)|}; + IReadOnlyList result = {|λ1013:Enumerable.Return(0)|}; return result; } } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -39,15 +39,15 @@ IReadOnlyList M() [Fact] public async Task SuggestsCollectionExpressionWhenTargetTypeIsIEnumerable() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { - IEnumerable M() => {|λ1013:Sequence.Return(0)|}; + IEnumerable M() => {|λ1013:Enumerable.Return(0)|}; } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -61,17 +61,17 @@ class C [Fact] public async Task SuggestsCollectionExpressionInArgumentPosition() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void Consume(IReadOnlyList values) { } - void M() => Consume({|λ1013:Sequence.Return(0)|}); + void M() => Consume({|λ1013:Enumerable.Return(0)|}); } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -87,15 +87,15 @@ void Consume(IReadOnlyList values) { } [Fact] public async Task CodeFixKeepsACompoundElementExpression() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { - IReadOnlyList M(int x) => {|λ1013:Sequence.Return(x + 1)|}; + IReadOnlyList M(int x) => {|λ1013:Enumerable.Return(x + 1)|}; } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -109,15 +109,15 @@ class C [Fact] public async Task CodeFixParenthesizesARangeWithoutLeftOperandSoItIsNotASpread() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { - IReadOnlyList M() => {|λ1013:Sequence.Return(..5)|}; + IReadOnlyList M() => {|λ1013:Enumerable.Return(..5)|}; } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -131,15 +131,15 @@ class C [Fact] public async Task CodeFixKeepsCommentsInsideTheInvocation() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { - IReadOnlyList M() => {|λ1013:Sequence.Return(/* explanation */ 0 /* trailing */)|}; + IReadOnlyList M() => {|λ1013:Enumerable.Return(/* explanation */ 0 /* trailing */)|}; } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -153,16 +153,16 @@ class C [Fact] public async Task CodeFixKeepsTheLineBreakAfterALineComment() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { - IReadOnlyList M() => {|λ1013:Sequence.Return(0 // explanation + IReadOnlyList M() => {|λ1013:Enumerable.Return(0 // explanation )|}; } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -177,19 +177,19 @@ IReadOnlyList M() => [0 // explanation [Fact] public async Task SuggestsCollectionExpressionWhenAssignedToAVariableNamedUnderscore() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void M() { IReadOnlyList _; - _ = {|λ1013:Sequence.Return(0)|}; + _ = {|λ1013:Enumerable.Return(0)|}; } } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -207,18 +207,18 @@ void M() [Fact] public async Task SuggestsCollectionExpressionInReturnOfATargetTypedLambda() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void M() { - System.Func> f = () => { return {|λ1013:Sequence.Return(0)|}; }; + System.Func> f = () => { return {|λ1013:Enumerable.Return(0)|}; }; } } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -235,17 +235,17 @@ void M() [Fact] public async Task SuggestsCollectionExpressionWhenTheElementTypeIsInferredFromTheCollection() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void Consume(IEnumerable values) { } - void M() => Consume({|λ1013:Sequence.Return(0)|}); + void M() => Consume({|λ1013:Enumerable.Return(0)|}); } """); - var fixedSource = WithSequenceStub( + var fixedSource = WithEnumerableStub( """ class C { @@ -261,13 +261,13 @@ void Consume(IEnumerable values) { } [Fact] public async Task DoesNotSuggestWhenAssignedToADiscard() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void M() { - _ = Sequence.Return(0); + _ = Enumerable.Return(0); } } """); @@ -278,7 +278,7 @@ void M() [Fact] public async Task DoesNotSuggestInsideAnExpressionTree() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { @@ -286,7 +286,7 @@ void Consume(IReadOnlyList values) { } void M() { - System.Linq.Expressions.Expression e = () => Consume(Sequence.Return(0)); + System.Linq.Expressions.Expression e = () => Consume(Enumerable.Return(0)); } } """); @@ -297,7 +297,7 @@ void M() [Fact] public async Task DoesNotSuggestWhenTheTargetElementTypeDiffersFromTheReturnedElementType() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class Widget { } @@ -305,8 +305,8 @@ class C { void M() { - IEnumerable widgets = Sequence.Return(new Widget()); - IEnumerable names = Sequence.Return("tag"); + IEnumerable widgets = Enumerable.Return(new Widget()); + IEnumerable names = Enumerable.Return("tag"); } } """); @@ -317,13 +317,13 @@ void M() [Fact] public async Task DoesNotSuggestWhenTheTypeParameterCouldNotBeInferredFromACollectionExpression() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void Consume(T value) { } - void M() => Consume(Sequence.Return(0)); + void M() => Consume(Enumerable.Return(0)); } """); @@ -333,7 +333,7 @@ void Consume(T value) { } [Fact] public async Task DoesNotSuggestWhenACollectionExpressionWouldPickAnotherOverload() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { @@ -341,7 +341,7 @@ void Consume(IEnumerable values) { } void Consume(int[] values) { } - void M() => Consume(Sequence.Return(0)); + void M() => Consume(Enumerable.Return(0)); } """); @@ -351,13 +351,13 @@ void Consume(int[] values) { } [Fact] public async Task DoesNotSuggestInReturnOfALambdaWithInferredType() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void M() { - var f = () => { return Sequence.Return(0); }; + var f = () => { return Enumerable.Return(0); }; } } """); @@ -368,13 +368,13 @@ void M() [Fact] public async Task DoesNotSuggestForImplicitlyTypedLocal() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void M() { - var result = Sequence.Return(0); + var result = Enumerable.Return(0); } } """); @@ -385,13 +385,13 @@ void M() [Fact] public async Task DoesNotSuggestWhenTargetTypeIsNotACollection() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { void M() { - object result = Sequence.Return(0); + object result = Enumerable.Return(0); } } """); @@ -402,11 +402,11 @@ void M() [Fact] public async Task DoesNotSuggestForParamsOverloadWithMultipleElements() { - var source = WithSequenceStub( + var source = WithEnumerableStub( """ class C { - IReadOnlyList M() => Sequence.Return(0, 1, 2); + IReadOnlyList M() => Enumerable.Return(0, 1, 2); } """); @@ -416,13 +416,13 @@ class C [Fact] public async Task DoesNotSuggestWhenLanguageVersionIsBelowCSharp12() { - var source = WithSequenceStub( + var source = WithClassicStub( """ class C { IReadOnlyList M() { - IReadOnlyList result = Sequence.Return(0); + IReadOnlyList result = EnumerableExtensions.Return(0); return result; } } @@ -438,18 +438,40 @@ IReadOnlyList M() await test.RunAsync(CancellationToken.None); } - private static string WithSequenceStub(string code) + // Enumerable.Return is a static extension member on System.Linq.Enumerable, declared in Funcky.Extensions.EnumerableExtensions. + private static string WithEnumerableStub(string code) => $$""" using System.Collections.Generic; - using Funcky; + using System.Linq; + using Funcky.Extensions; - namespace Funcky + namespace Funcky.Extensions { - public static class Sequence + public static class EnumerableExtensions { - public static IReadOnlyList Return(TResult element) => new[] { element }; + extension(Enumerable) + { + public static IReadOnlyList Return(TResult element) => new[] { element }; + + public static IReadOnlyList Return(params TResult[] elements) => elements; + } + } + } + + {{code}} + """; - public static IReadOnlyList Return(params TResult[] elements) => elements; + // Extension blocks need C# 14, so the test for older language versions uses the lowered form of the same member. + private static string WithClassicStub(string code) + => $$""" + using System.Collections.Generic; + using Funcky.Extensions; + + namespace Funcky.Extensions + { + public static class EnumerableExtensions + { + public static IReadOnlyList Return(TResult element) => new[] { element }; } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.expected index d7e0e5916..1470047d4 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.expected +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.expected @@ -1,10 +1,10 @@ using System; using System.Linq; -using Funcky; +using Funcky.Extensions; -namespace Funcky { - class Sequence { - public static string Return(string value) => value; +namespace Funcky.Extensions { + public static class EnumerableExtensions { + extension(Enumerable) { public static string Return(string value) => value; } } } @@ -14,7 +14,7 @@ namespace ConsoleApplication1 { private void Syntax() { - var single = Sequence.Return("Hello world!"); + var single = Enumerable.Return("Hello world!"); } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.input index d1c27a895..0739b90a9 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.input +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnce.input @@ -1,10 +1,10 @@ using System; using System.Linq; -using Funcky; +using Funcky.Extensions; -namespace Funcky { - class Sequence { - public static string Return(string value) => value; +namespace Funcky.Extensions { + public static class EnumerableExtensions { + extension(Enumerable) { public static string Return(string value) => value; } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected index 8f33adfcc..ad6bd4a12 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected @@ -1,12 +1,12 @@ using System; using System.Linq; -using Funcky; +using Funcky.Extensions; -namespace Funcky +namespace Funcky.Extensions { - class Sequence + public static class EnumerableExtensions { - public static string Return(string value) => value; + extension(Enumerable) { public static string Return(string value) => value; } } } @@ -16,7 +16,7 @@ namespace ConsoleApplication1 { private void Syntax() { - var single = Sequence.Return("Hello world!"); + var single = Enumerable.Return("Hello world!"); } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input index d1dabdaee..29f6e33fb 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input @@ -1,12 +1,12 @@ using System; using System.Linq; -using Funcky; +using Funcky.Extensions; -namespace Funcky +namespace Funcky.Extensions { - class Sequence + public static class EnumerableExtensions { - public static string Return(string value) => value; + extension(Enumerable) { public static string Return(string value) => value; } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceMissingSequenceType.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceMissingEnumerableExtensions.input similarity index 100% rename from Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceMissingSequenceType.input rename to Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceMissingEnumerableExtensions.input diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.expected index b7b31bb49..438e763e8 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.expected +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.expected @@ -1,38 +1,45 @@ -namespace Funcky +namespace Funcky.Extensions { - class Sequence + public static class EnumerableExtensions { - public static System.Collections.Generic.IEnumerable Return(T value) => null!; + extension(System.Linq.Enumerable) + { + public static System.Collections.Generic.IEnumerable Return(T value) => null!; + } } } -namespace WithUsingStatic +namespace WithUsing { - using static Funcky.Sequence; + using System.Linq; + using Funcky.Extensions; public class C { public void M() { - _ = Return(10); + _ = Enumerable.Return(10); } } } namespace WithFullyQualifiedName { + using Funcky.Extensions; + public class C { public void M() { - _ = Funcky.Sequence.Return(10); + _ = System.Linq.Enumerable.Return(10); } } } namespace WithUsingAlias { - using Seq = Funcky.Sequence; + using Funcky.Extensions; + using Seq = System.Linq.Enumerable; public class C { diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.input index 0e9f19fc1..b1f4c1aab 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.input +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceQualification.input @@ -1,26 +1,32 @@ -namespace Funcky +namespace Funcky.Extensions { - class Sequence + public static class EnumerableExtensions { - public static System.Collections.Generic.IEnumerable Return(T value) => null!; + extension(System.Linq.Enumerable) + { + public static System.Collections.Generic.IEnumerable Return(T value) => null!; + } } } -namespace WithUsingStatic +namespace WithUsing { - using static Funcky.Sequence; + using System.Linq; + using Funcky.Extensions; public class C { public void M() { - _ = System.Linq.Enumerable.Repeat(10, 1); + _ = Enumerable.Repeat(10, 1); } } } namespace WithFullyQualifiedName { + using Funcky.Extensions; + public class C { public void M() @@ -32,13 +38,14 @@ namespace WithFullyQualifiedName namespace WithUsingAlias { - using Seq = Funcky.Sequence; + using Funcky.Extensions; + using Seq = System.Linq.Enumerable; public class C { public void M() { - _ = System.Linq.Enumerable.Repeat(10, 1); + _ = Seq.Repeat(10, 1); } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.expected index fba47dbe3..a3e0c1555 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.expected +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.expected @@ -1,10 +1,10 @@ using System; using System.Linq; -using Funcky; +using Funcky.Extensions; -namespace Funcky { - class Sequence { - public static string Return(string value) => value; +namespace Funcky.Extensions { + public static class EnumerableExtensions { + extension(Enumerable) { public static string Return(string value) => value; } } } @@ -15,7 +15,7 @@ namespace ConsoleApplication1 private void Syntax() { const int once = 1; - var single = Sequence.Return("Hello world!"); + var single = Enumerable.Return("Hello world!"); } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.input index f8f0f254d..24536fbe8 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.input +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceWithConstant.input @@ -1,10 +1,10 @@ using System; using System.Linq; -using Funcky; +using Funcky.Extensions; -namespace Funcky { - class Sequence { - public static string Return(string value) => value; +namespace Funcky.Extensions { + public static class EnumerableExtensions { + extension(Enumerable) { public static string Return(string value) => value; } } } diff --git a/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md b/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md index f2b7fad65..686fe55a6 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md +++ b/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md @@ -1,2 +1,8 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### Removed Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +λ1202 | Funcky.Style | Warning | PreferEnumerableSequenceConstructorsAnalyzer, the `Sequence` class it pointed away from is removed in Funcky 4.0.0 +λ1203 | Funcky.Style | Warning | PreferXunitAssertAnalyzer, the `FunctionalAssert` class it pointed away from is removed in Funcky.Xunit 3.0.0 / Funcky.Xunit.v3 2.0.0 diff --git a/Funcky.Analyzers/Funcky.Analyzers/CodeAnalysisExtensions/SingleElementCollectionExpression.cs b/Funcky.Analyzers/Funcky.Analyzers/CodeAnalysisExtensions/SingleElementCollectionExpression.cs index 9cc0c7b69..6d59ba50f 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/CodeAnalysisExtensions/SingleElementCollectionExpression.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/CodeAnalysisExtensions/SingleElementCollectionExpression.cs @@ -3,7 +3,7 @@ namespace Funcky.Analyzers.CodeAnalysisExtensions; -/// Builds the collection expression [element] that replaces Sequence.Return(element). Shared between the +/// Builds the collection expression [element] that replaces Enumerable.Return(element). Shared between the /// analyzer, which binds it speculatively, and the code fix, which inserts it. internal static class SingleElementCollectionExpression { diff --git a/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs b/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs index 23ff1dfdf..5fd6308fd 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs @@ -6,9 +6,11 @@ internal static class DiagnosticName public const string Usage = "10"; + /// The rules for the Funcky.Xunit assertions; the range is named after the FunctionalAssert class they were born with. public const string FunctionalAssertUsage = "11"; - /// The rules that point at the C# 14 extension forms added in 3.8.0 (static extension members next to the classic API). + /// The rules that point at the C# 14 extension forms added in 3.8.0 (static extension members next to the classic API). + /// λ1202 and λ1203 retired with Funcky 4.0.0, which removed the classic forms they pointed away from. public const string ExtensionForms = "12"; /// The category of every opt-in rule: the flagged code works, but Funcky offers a more idiomatic form. diff --git a/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs index 8accb5636..bb45aedcb 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs @@ -1,8 +1,10 @@ using System.Collections.Immutable; using Funcky.Analyzers.CodeAnalysisExtensions; +using Funcky.Analyzers.ExtensionForms; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using static Funcky.Analyzers.FunckyWellKnownMemberNames; using static Funcky.Analyzers.LocalizedResourceLoader; using static Funcky.Analyzers.OperationMatching; using static Funcky.Analyzers.Resources; @@ -32,8 +34,10 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { + // Enumerable.Return is a static extension member Funcky declares on System.Linq.Enumerable. if (context.Compilation.GetEnumerableType() is { } enumerableType - && context.Compilation.GetSequenceType() is not null) + && context.Compilation.GetEnumerableExtensionType() is { } enumerableExtensionsType + && ExtensionMemberSupport.HasStaticMember(enumerableExtensionsType, MonadReturnMethodName)) { context.RegisterOperationAction(FindEnumerableRepeatOnce(enumerableType), OperationKind.Invocation); } diff --git a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/ExtensionMemberSupport.cs b/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/ExtensionMemberSupport.cs index f22fbb807..4eb9647fc 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/ExtensionMemberSupport.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/ExtensionMemberSupport.cs @@ -1,22 +1,14 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; namespace Funcky.Analyzers.ExtensionForms; -/// What the rules for the C# 14 extension forms share: the language gate and the lookup of members that -/// live in an extension block, which the compiler nests in an unnamed extension type inside the static class. +/// What the rules for the C# 14 extension forms share: the lookup of members that live in an extension block, +/// which the compiler nests in an unnamed extension type inside the static class. There is no language gate: Funcky 4 +/// requires the .NET 10 SDK, whose compiler binds the static extension members. public static class ExtensionMemberSupport { public const string ExtensionsNamespace = "Funcky.Extensions"; - // 1400 is the numeric value of LanguageVersion.CSharp14; the analyzer is compiled against Roslyn versions that predate it. - private const int CSharp14 = 1400; - - /// Whether the file is compiled with C# 14 or newer; on an older language version the static extension members do not bind. - public static bool SupportsExtensionMembers(SyntaxTree syntaxTree) - => syntaxTree.Options is CSharpParseOptions parseOptions - && (int)parseOptions.LanguageVersion.MapSpecifiedToEffectiveVersion() >= CSharp14; - /// Whether the static class declares a member of that name, directly or in one of its extension blocks. public static bool HasMember(INamedTypeSymbol type, string name) => type.GetMembers(name).Any() diff --git a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferEnumerableSequenceConstructorsAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferEnumerableSequenceConstructorsAnalyzer.cs deleted file mode 100644 index 30349e469..000000000 --- a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferEnumerableSequenceConstructorsAnalyzer.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Immutable; -using Funcky.Analyzers.CodeAnalysisExtensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; -using static Funcky.Analyzers.ExtensionForms.ExtensionMemberSupport; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Funcky.Analyzers.ExtensionForms; - -/// Points from the constructors of Funcky.Sequence to their static extension members on -/// System.Linq.Enumerable: Sequence.Successors(1, n => n + 1) is Enumerable.Successors(1, n => n + 1). -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class PreferEnumerableSequenceConstructorsAnalyzer : DiagnosticAnalyzer -{ - public static readonly DiagnosticDescriptor PreferEnumerable = new( - id: $"{DiagnosticName.Prefix}{DiagnosticName.ExtensionForms}02", - title: "Prefer the Enumerable form of the Sequence constructor", - messageFormat: "Replace 'Sequence.{0}' with 'Enumerable.{0}'", - category: DiagnosticName.Category, - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - helpLinkUri: DiagnosticName.HelpLink($"{DiagnosticName.Prefix}{DiagnosticName.ExtensionForms}02")); - - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(PreferEnumerable); - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) - { - if (context.Compilation.GetSequenceType() is { } sequenceType - && context.Compilation.GetEnumerableExtensionType() is { } enumerableExtensionsType - && context.Compilation.GetEnumerableType() is { } enumerableType) - { - var expressionOfTType = context.Compilation.GetExpressionOfTType(); - context.RegisterOperationAction(AnalyzeInvocation(sequenceType, enumerableExtensionsType, enumerableType, expressionOfTType), OperationKind.Invocation); - } - } - - private static Action AnalyzeInvocation(INamedTypeSymbol sequenceType, INamedTypeSymbol enumerableExtensionsType, INamedTypeSymbol enumerableType, INamedTypeSymbol? expressionOfTType) - => context => - { - var operation = (IInvocationOperation)context.Operation; - var semanticModel = operation.SemanticModel ?? throw new InvalidOperationException("Semantic model is never null for operations passed to an analyzer (according to docs)"); - - if (SymbolEquals(operation.TargetMethod.ContainingType, sequenceType) - && SupportsExtensionMembers(operation.Syntax.SyntaxTree) - && HasMember(enumerableExtensionsType, operation.TargetMethod.Name) - && operation.Syntax is InvocationExpressionSyntax invocation - && EnumerableFormBinds(invocation, semanticModel, enumerableExtensionsType, enumerableType) - && !operation.Syntax.IsInExpressionTree(semanticModel, expressionOfTType, context.CancellationToken)) - { - context.ReportDiagnostic(Diagnostic.Create(PreferEnumerable, operation.Syntax.GetLocation(), operation.TargetMethod.Name)); - } - }; - - // The Enumerable form is the same call with `Enumerable` as the receiver. It is reported when that call binds to the - // Enumerable block of EnumerableExtensions, or when it binds to nothing at all because `Funcky.Extensions` is not - // imported yet, which the code fix takes care of. A call that binds elsewhere (an overload of Enumerable itself) is left alone. - private static bool EnumerableFormBinds(InvocationExpressionSyntax invocation, SemanticModel semanticModel, INamedTypeSymbol enumerableExtensionsType, INamedTypeSymbol enumerableType) - { - if (EnumerableForm(invocation, enumerableType) is not { } replacement) - { - return false; - } - - var symbolInfo = semanticModel.GetSpeculativeSymbolInfo(invocation.SpanStart, replacement, SpeculativeBindingOption.BindAsExpression); - return symbolInfo.Symbol is { } symbol - ? SymbolEquals(DeclaringClass(symbol), enumerableExtensionsType) - : symbolInfo.CandidateSymbols.IsEmpty - && !IsVisible(enumerableExtensionsType, semanticModel, invocation.SpanStart) // when the class is visible by its simple name, `Funcky.Extensions` is imported, so a call that binds to nothing has no static form - && HasStaticMember(enumerableExtensionsType, invocation.Expression switch { MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText, SimpleNameSyntax name => name.Identifier.ValueText, _ => string.Empty }); - } - - private static InvocationExpressionSyntax? EnumerableForm(InvocationExpressionSyntax invocation, INamedTypeSymbol enumerableType) - { - var enumerable = ParseExpression(enumerableType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - return invocation.Expression switch - { - MemberAccessExpressionSyntax memberAccess => invocation.WithExpression(memberAccess.WithExpression(enumerable)), - SimpleNameSyntax name => invocation.WithExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, enumerable, name)), - _ => null, - }; - } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferStaticParseOrNoneAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferStaticParseOrNoneAnalyzer.cs index face1c061..34735022c 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferStaticParseOrNoneAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferStaticParseOrNoneAnalyzer.cs @@ -74,7 +74,6 @@ private static Action AnalyzeInvocation(INamedTypeSymb var semanticModel = operation.SemanticModel ?? throw new InvalidOperationException("Semantic model is never null for operations passed to an analyzer (according to docs)"); if (IsParseOrNoneExtension(operation.TargetMethod, parseExtensionsType) - && SupportsExtensionMembers(operation.Syntax.SyntaxTree) && TargetType(operation.TargetMethod, optionOfTType) is { } targetType && !operation.Syntax.IsInExpressionTree(semanticModel, expressionOfTType, context.CancellationToken) && StaticMemberName(operation.TargetMethod.Name) is var staticMemberName diff --git a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferXunitAssertAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferXunitAssertAnalyzer.cs deleted file mode 100644 index 15e0535bc..000000000 --- a/Funcky.Analyzers/Funcky.Analyzers/ExtensionForms/PreferXunitAssertAnalyzer.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Immutable; -using Funcky.Analyzers.CodeAnalysisExtensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; -using static Funcky.Analyzers.ExtensionForms.ExtensionMemberSupport; - -namespace Funcky.Analyzers.ExtensionForms; - -/// Points from FunctionalAssert to the same assertions as extension members of xunit's Assert: -/// FunctionalAssert.Some(option) is Assert.Some(option). -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class PreferXunitAssertAnalyzer : DiagnosticAnalyzer -{ - public static readonly DiagnosticDescriptor PreferAssert = new( - id: $"{DiagnosticName.Prefix}{DiagnosticName.ExtensionForms}03", - title: "Prefer the assertion on Assert over FunctionalAssert", - messageFormat: "Replace 'FunctionalAssert.{0}' with 'Assert.{0}'", - category: DiagnosticName.Category, - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - helpLinkUri: DiagnosticName.HelpLink($"{DiagnosticName.Prefix}{DiagnosticName.ExtensionForms}03")); - - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(PreferAssert); - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(OnCompilationStart); - } - - private static void OnCompilationStart(CompilationStartAnalysisContext context) - { - if (context.Compilation.GetFunctionalAssertType() is { } functionalAssertType - && context.Compilation.GetAssertExtensionsType() is { } assertExtensionsType - && context.Compilation.GetXunitAssertType() is not null) - { - var expressionOfTType = context.Compilation.GetExpressionOfTType(); - context.RegisterOperationAction(AnalyzeInvocation(functionalAssertType, assertExtensionsType, expressionOfTType), OperationKind.Invocation); - } - } - - // Static extension members cannot be invoked in an expression tree, so FunctionalAssert stays the only form there. - private static Action AnalyzeInvocation(INamedTypeSymbol functionalAssertType, INamedTypeSymbol assertExtensionsType, INamedTypeSymbol? expressionOfTType) - => context => - { - var operation = (IInvocationOperation)context.Operation; - var semanticModel = operation.SemanticModel ?? throw new InvalidOperationException("Semantic model is never null for operations passed to an analyzer (according to docs)"); - - if (SymbolEquals(operation.TargetMethod.ContainingType, functionalAssertType) - && SupportsExtensionMembers(operation.Syntax.SyntaxTree) - && HasMember(assertExtensionsType, operation.TargetMethod.Name) - && !operation.Syntax.IsInExpressionTree(semanticModel, expressionOfTType, context.CancellationToken)) - { - context.ReportDiagnostic(Diagnostic.Create(PreferAssert, operation.Syntax.GetLocation(), operation.TargetMethod.Name)); - } - }; -} diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs index 14cc57950..d14da8755 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs @@ -28,8 +28,6 @@ public static class FunckyWellKnownTypeNames public static INamedTypeSymbol? GetResultOfTType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Monads.Result`1"); - public static INamedTypeSymbol? GetSequenceType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Sequence"); - public static INamedTypeSymbol? GetFunctionalType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Functional"); public static INamedTypeSymbol? GetExpressionOfTType(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Linq.Expressions.Expression`1"); @@ -38,8 +36,6 @@ public static class FunckyWellKnownTypeNames public static INamedTypeSymbol? GetXunitAssertType(this Compilation compilation) => compilation.GetTypeByMetadataName("Xunit.Assert"); - public static INamedTypeSymbol? GetFunctionalAssertType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.FunctionalAssert"); - public static INamedTypeSymbol? GetAssertExtensionsType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Extensions.AssertExtensions"); public static INamedTypeSymbol? GetParseExtensionsType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Extensions.ParseExtensions"); diff --git a/Funcky.Analyzers/Funcky.Analyzers/Resources.Designer.cs b/Funcky.Analyzers/Funcky.Analyzers/Resources.Designer.cs index aa66bb887..42df8d69f 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/Resources.Designer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/Resources.Designer.cs @@ -88,7 +88,7 @@ internal static string EnumerableRepeatNeverAnalyzerTitle { } /// - /// Looks up a localized string similar to Funcky provides Sequence.Return to create an IEnumerable of a single item. + /// Looks up a localized string similar to Funcky provides Enumerable.Return to create an IEnumerable of a single item. /// internal static string EnumerableRepeatOnceAnalyzerDescription { get { @@ -97,7 +97,7 @@ internal static string EnumerableRepeatOnceAnalyzerDescription { } /// - /// Looks up a localized string similar to Use Sequence.Return({0}) instead of Enumerable.Repeat({0}, 1) . + /// Looks up a localized string similar to Use Enumerable.Return({0}) instead of Enumerable.Repeat({0}, 1). /// internal static string EnumerableRepeatOnceAnalyzerMessageFormat { get { diff --git a/Funcky.Analyzers/Funcky.Analyzers/Resources.resx b/Funcky.Analyzers/Funcky.Analyzers/Resources.resx index 0411a5986..c2ee14275 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/Resources.resx +++ b/Funcky.Analyzers/Funcky.Analyzers/Resources.resx @@ -118,11 +118,11 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Funcky provides Sequence.Return to create an IEnumerable of a single item + Funcky provides Enumerable.Return to create an IEnumerable of a single item The title of the diagnostic. - Use Sequence.Return({0}) instead of Enumerable.Repeat({0}, 1) + Use Enumerable.Return({0}) instead of Enumerable.Repeat({0}, 1) The format-able message the diagnostic displays. diff --git a/Funcky.Analyzers/Funcky.Analyzers/SequenceReturnCollectionExpressionAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/SequenceReturnCollectionExpressionAnalyzer.cs index 1b78c6dbc..2ac7e37be 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/SequenceReturnCollectionExpressionAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/SequenceReturnCollectionExpressionAnalyzer.cs @@ -6,7 +6,6 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using static Funcky.Analyzers.FunckyWellKnownMemberNames; -using static Funcky.Analyzers.OperationMatching; namespace Funcky.Analyzers; @@ -15,8 +14,8 @@ public sealed class SequenceReturnCollectionExpressionAnalyzer : DiagnosticAnaly { public static readonly DiagnosticDescriptor UseCollectionExpression = new( id: $"{DiagnosticName.Prefix}{DiagnosticName.Usage}13", - title: "Sequence.Return can be replaced with a collection expression", - messageFormat: "Replace 'Sequence.Return' with a collection expression", + title: "Enumerable.Return can be replaced with a collection expression", + messageFormat: "Replace 'Enumerable.Return' with a collection expression", category: DiagnosticName.Category, DiagnosticSeverity.Info, isEnabledByDefault: true, @@ -33,21 +32,22 @@ public override void Initialize(AnalysisContext context) private static void OnCompilationStart(CompilationStartAnalysisContext context) { - if (context.Compilation.GetSequenceType() is { } sequenceType) + // Enumerable.Return is a static extension member Funcky declares on System.Linq.Enumerable in EnumerableExtensions. + if (context.Compilation.GetEnumerableExtensionType() is { } enumerableExtensionsType) { var collectionTargetTypes = GetCollectionExpressionTargetTypes(context.Compilation); var expressionOfTType = context.Compilation.GetExpressionOfTType(); - context.RegisterOperationAction(AnalyzeInvocation(sequenceType, collectionTargetTypes, expressionOfTType), OperationKind.Invocation); + context.RegisterOperationAction(AnalyzeInvocation(enumerableExtensionsType, collectionTargetTypes, expressionOfTType), OperationKind.Invocation); } } - private static Action AnalyzeInvocation(INamedTypeSymbol sequenceType, ImmutableArray collectionTargetTypes, INamedTypeSymbol? expressionOfTType) + private static Action AnalyzeInvocation(INamedTypeSymbol enumerableExtensionsType, ImmutableArray collectionTargetTypes, INamedTypeSymbol? expressionOfTType) => context => { var operation = (IInvocationOperation)context.Operation; var semanticModel = operation.SemanticModel ?? throw new InvalidOperationException("Semantic model is never null for operations passed to an analyzer (according to docs)"); - if (MatchSingleElementSequenceReturn(operation, sequenceType) + if (MatchSingleElementEnumerableReturn(operation, enumerableExtensionsType) && SupportsCollectionExpressions(operation.Syntax.SyntaxTree) && IsInTargetTypedContext(operation.Syntax) && !IsAssignedToDiscard(operation) @@ -61,10 +61,11 @@ private static Action AnalyzeInvocation(INamedTypeSymb // We deliberately only match the single-element overload: a collection expression `[x]` always // matches it, whereas the params overload may be called in normal form with an existing array - // (`Sequence.Return(array)`), where `[array]` would change the meaning. - private static bool MatchSingleElementSequenceReturn(IInvocationOperation operation, INamedTypeSymbol sequenceType) - => MatchMethod(operation, sequenceType, MonadReturnMethodName) - && operation.TargetMethod.Parameters is [{ IsParams: false }]; + // (`Enumerable.Return(array)`), where `[array]` would change the meaning. The member lives in an + // extension block, so its containing type is the unnamed extension type nested in EnumerableExtensions. + private static bool MatchSingleElementEnumerableReturn(IInvocationOperation operation, INamedTypeSymbol enumerableExtensionsType) + => operation.TargetMethod is { Name: MonadReturnMethodName, IsStatic: true, Parameters: [{ IsParams: false }], ContainingType: { } containingType } + && (SymbolEquals(containingType, enumerableExtensionsType) || SymbolEquals(containingType.ContainingType, enumerableExtensionsType)); // 1200 is the numeric value of LanguageVersion.CSharp12; we can't reference the enum member // directly because the analyzer is also compiled against Roslyn versions that predate it. @@ -86,7 +87,7 @@ private static bool IsAssignedToDiscard(IOperation operation) return operation; } - // The element type has to stay the same: `IEnumerable x = Sequence.Return(new())` constructs a + // The element type has to stay the same: `IEnumerable x = Enumerable.Return(new())` constructs a // Widget, whereas `[new()]` would construct an object, and a user-defined conversion to TResult would disappear. private static bool HasCollectionTargetTypeWithSameElementType(IInvocationOperation operation, ImmutableArray collectionTargetTypes) => operation.SemanticModel?.GetTypeInfo(operation.Syntax).ConvertedType is INamedTypeSymbol { TypeArguments: [var elementType] } convertedType @@ -169,7 +170,7 @@ private static bool ReplacementBindsToTheSameMember(IInvocationOperation operati return originalSymbol is not null && SymbolEquals(originalSymbol, replacementSymbol); } - // `Sequence.Return` returns IReadOnlyList, so the only target types the existing code can + // `Enumerable.Return` returns IReadOnlyList, so the only target types the existing code can // already convert to (and that collection expressions support) are these read-only interfaces. private static ImmutableArray GetCollectionExpressionTargetTypes(Compilation compilation) => new[] diff --git a/Funcky.Analyzers/readme.md b/Funcky.Analyzers/readme.md index d94f79cc0..f8332ce64 100644 --- a/Funcky.Analyzers/readme.md +++ b/Funcky.Analyzers/readme.md @@ -25,7 +25,8 @@ EF Core ship their usage diagnostics inside the library package, whereas best-pr ### Rule IDs Rule IDs are stable identifiers and never change once shipped, even when a rule moves between the packages (λ1009 and λ1010 moved to the built-in analyzers in Funcky 3.7.0 and kept their numbers). The number range records -where a rule was born: `λ00xx` built-in, `λ10xx` `Funcky.Analyzers`, `λ11xx` `FunctionalAssert` rules. The rules table in +where a rule was born: `λ00xx` built-in, `λ10xx` `Funcky.Analyzers`, `λ11xx` rules for the Funcky.Xunit assertions, `λ12xx` +rules for the C# 14 extension forms. A retired rule (λ1202, λ1203) keeps its number reserved. The rules table in the documentation is the authority on where a rule lives. ### Built-in Analyzers and Roslyn Versions diff --git a/Funcky.Test/Extensions/SequenceTest.cs b/Funcky.Test/Extensions/SequenceTest.cs index c5a9822a4..66fe69a69 100644 --- a/Funcky.Test/Extensions/SequenceTest.cs +++ b/Funcky.Test/Extensions/SequenceTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - namespace Funcky.Test.Extensions; public sealed class SequenceTest @@ -7,45 +5,45 @@ public sealed class SequenceTest [Fact] public void GivenAnObjectWeCreateAnIEnumerableWithToEnumerable() { - AcceptIntegers(Sequence.Return(42)); + AcceptIntegers(Enumerable.Return(42)); Unit unit; - AcceptUnits(Sequence.Return(unit)); + AcceptUnits(Enumerable.Return(unit)); } [Fact] public void ToEnumerableReturnsEmptyEnumerableWhenReferenceIsNull() { string? value = null; - Assert.Empty(Sequence.FromNullable(value)); + Assert.Empty(Enumerable.FromNullable(value)); } [Fact] public void ToEnumerableReturnsEnumerableWithOneValueWhenReferenceIsNotNull() { const string value = "foo"; - Assert.Single(Sequence.FromNullable(value), value); + Assert.Single(Enumerable.FromNullable(value), value); } [Fact] public void ToEnumerableReturnsEmptyEnumerableWhenNullableValueTypeIsNull() { int? value = null; - Assert.Empty(Sequence.FromNullable(value)); + Assert.Empty(Enumerable.FromNullable(value)); } [Fact] public void ToEnumerableReturnsEnumerableWithOneValueWhenNullableValueTypeIsNotNull() { int? value = 10; - Assert.Single(Sequence.FromNullable(value), value); + Assert.Single(Enumerable.FromNullable(value), value); } [Fact] public void ToEnumerableReturnsEnumerableWithOneValueWhenValueTypeIsNotNull() { const int value = 10; - Assert.Single(Sequence.Return(value), value); + Assert.Single(Enumerable.Return(value), value); } private static void AcceptIntegers(IEnumerable values) diff --git a/Funcky.Test/Sequence/ConcatTest.cs b/Funcky.Test/Sequence/ConcatTest.cs index f64429871..5e081d1e3 100644 --- a/Funcky.Test/Sequence/ConcatTest.cs +++ b/Funcky.Test/Sequence/ConcatTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using System.Collections.Immutable; using FsCheck; using FsCheck.Fluent; @@ -12,20 +10,20 @@ public sealed class ConcatTest [Fact] public void ConcatenatedSequenceIsEmptyWhenNoSourcesAreProvided() { - Assert.Empty(Sequence.Concat()); + Assert.Empty(Enumerable.Concat()); } [Fact] public void ConcatenatedSequenceIsEmptyWhenAllSourcesAreEmpty() { - Assert.Empty(Sequence.Concat(ImmutableArray.Create(Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty()))); + Assert.Empty(Enumerable.Concat(ImmutableArray.Create(Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty()))); } [Property] public Property ConcatenatedSequenceContainsElementsFromAllSourcesInOrder(int[][] sources) { var expected = sources.Aggregate(ImmutableArray.Empty, (l, s) => l.AddRange(s)); - var actual = Sequence.Concat(sources); + var actual = Enumerable.Concat(sources); return expected.SequenceEqual(actual).ToProperty(); } } diff --git a/Funcky.Test/Sequence/CycleMaterializedTest.cs b/Funcky.Test/Sequence/CycleMaterializedTest.cs index 8b65b5606..875e0dd77 100644 --- a/Funcky.Test/Sequence/CycleMaterializedTest.cs +++ b/Funcky.Test/Sequence/CycleMaterializedTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -13,7 +11,7 @@ public sealed class CycleMaterializedTest public void IsEnumeratedLazily() { var doNotEnumerate = new FailOnEnumerateReadOnlyCollection(Count: 1); - _ = Sequence.CycleMaterialized(doNotEnumerate); + _ = Enumerable.CycleMaterialized(doNotEnumerate); } [Fact] @@ -23,7 +21,7 @@ public void CyclingAnEmptySetThrowsAnException() [Property] public Property CanProduceArbitraryManyItems(NonEmptySet sequence, PositiveInt arbitraryElements) { - var cycleRange = Sequence.CycleMaterialized(sequence.Get.Materialize()); + var cycleRange = Enumerable.CycleMaterialized(sequence.Get.Materialize()); return (cycleRange.Take(arbitraryElements.Get).Count() == arbitraryElements.Get) .ToProperty(); @@ -32,7 +30,7 @@ public Property CanProduceArbitraryManyItems(NonEmptySet sequence, Positive [Property] public Property RepeatsTheElementsArbitraryManyTimes(NonEmptySet sequence, PositiveInt arbitraryElements) { - var cycleRange = Sequence.CycleMaterialized(sequence.Get.Materialize()); + var cycleRange = Enumerable.CycleMaterialized(sequence.Get.Materialize()); return cycleRange .IsSequenceRepeating(sequence.Get) @@ -42,7 +40,7 @@ public Property RepeatsTheElementsArbitraryManyTimes(NonEmptySet sequence, private static void CycleEmptySequence() { - var cycledRange = Sequence.CycleMaterialized(Array.Empty()); + var cycledRange = Enumerable.CycleMaterialized(Array.Empty()); using var enumerator = cycledRange.GetEnumerator(); enumerator.MoveNext(); diff --git a/Funcky.Test/Sequence/CycleRangeTest.cs b/Funcky.Test/Sequence/CycleRangeTest.cs index ef5b28af5..67722a16e 100644 --- a/Funcky.Test/Sequence/CycleRangeTest.cs +++ b/Funcky.Test/Sequence/CycleRangeTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -14,7 +12,7 @@ public void CycleRangeIsEnumeratedLazily() { var doNotEnumerate = new FailOnEnumerationSequence(); - using var cycleRange = Sequence.CycleRange(doNotEnumerate); + using var cycleRange = Enumerable.CycleRange(doNotEnumerate); } [Fact] @@ -24,7 +22,7 @@ public void CyclingAnEmptySetThrowsAnException() [Property] public Property CycleRangeCanProduceArbitraryManyItems(NonEmptySet sequence, PositiveInt arbitraryElements) { - using var cycleRange = Sequence + using var cycleRange = Enumerable .CycleRange(sequence.Get); return (cycleRange.Take(arbitraryElements.Get).Count() == arbitraryElements.Get) @@ -34,7 +32,7 @@ public Property CycleRangeCanProduceArbitraryManyItems(NonEmptySet sequence [Property] public Property CycleRangeRepeatsTheElementsArbitraryManyTimes(NonEmptySet sequence, PositiveInt arbitraryElements) { - using var cycleRange = Sequence + using var cycleRange = Enumerable .CycleRange(sequence.Get); return cycleRange @@ -46,10 +44,10 @@ public Property CycleRangeRepeatsTheElementsArbitraryManyTimes(NonEmptySet [Fact] public void CycleRangeEnumeratesUnderlyingEnumerableOnlyOnce() { - var sequence = Sequence.Return("Test", "Hello", "Do", "Wait"); + var sequence = Enumerable.Return("Test", "Hello", "Do", "Wait"); var enumerateOnce = EnumerateOnce.Create(sequence); - using var cycleRange = Sequence + using var cycleRange = Enumerable .CycleRange(enumerateOnce); cycleRange @@ -61,7 +59,7 @@ public void CycleRangeEnumeratesUnderlyingEnumerableOnlyOnce() public void CycleRangeAcquiresTheSourceEnumeratorOnlyWhenTheFirstElementIsRequested() { var source = new DisposeTrackingSequence([1, 2, 3]); - using var cycleRange = Sequence.CycleRange(source); + using var cycleRange = Enumerable.CycleRange(source); using var enumerator = cycleRange.GetEnumerator(); Assert.Equal(0, source.EnumeratorsCreated); @@ -74,7 +72,7 @@ public void CycleRangeAcquiresTheSourceEnumeratorOnlyWhenTheFirstElementIsReques public void CycleRangeDisposesTheSourceEnumeratorOnceTheSourceIsExhausted() { var source = new DisposeTrackingSequence([1, 2, 3]); - using var cycleRange = Sequence.CycleRange(source); + using var cycleRange = Enumerable.CycleRange(source); Assert.Equal([1, 2, 3, 1], cycleRange.Take(4)); Assert.Equal(1, source.EnumeratorsDisposed); @@ -85,7 +83,7 @@ public void CycleRangeDisposesTheSourceEnumeratorOnceTheSourceIsExhausted() private static void CycleEmptySequence() { - using var cycledRange = Sequence.CycleRange(Sequence.Return()); + using var cycledRange = Enumerable.CycleRange(Enumerable.Return()); using var enumerator = cycledRange.GetEnumerator(); enumerator.MoveNext(); diff --git a/Funcky.Test/Sequence/CycleTest.cs b/Funcky.Test/Sequence/CycleTest.cs index e41cc68a0..ec3206c82 100644 --- a/Funcky.Test/Sequence/CycleTest.cs +++ b/Funcky.Test/Sequence/CycleTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -11,14 +9,14 @@ public sealed class CycleTest { [Property] public Property CycleCanProduceArbitraryManyItems(int value, PositiveInt arbitraryElements) - => (Sequence.Cycle(value).Take(arbitraryElements.Get).Count() == arbitraryElements.Get) + => (Enumerable.Cycle(value).Take(arbitraryElements.Get).Count() == arbitraryElements.Get) .ToProperty(); [Property] public Property CycleRepeatsTheElementArbitraryManyTimes(int value, PositiveInt arbitraryElements) - => Sequence + => Enumerable .Cycle(value) - .IsSequenceRepeating(Sequence.Return(value)) + .IsSequenceRepeating(Enumerable.Return(value)) .NTimes(arbitraryElements.Get) .ToProperty(); } diff --git a/Funcky.Test/Sequence/EnumerableConstructorsTest.cs b/Funcky.Test/Sequence/EnumerableConstructorsTest.cs index 4f44129fc..600105f9f 100644 --- a/Funcky.Test/Sequence/EnumerableConstructorsTest.cs +++ b/Funcky.Test/Sequence/EnumerableConstructorsTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - namespace Funcky.Test; public sealed class EnumerableConstructorsTest @@ -7,7 +5,7 @@ public sealed class EnumerableConstructorsTest [Fact] public void ReturnIsAvailableOnEnumerable() { - Assert.Equal(Sequence.Return(42), Enumerable.Return(42)); + Assert.Equal(Enumerable.Return(42), Enumerable.Return(42)); Assert.Equal(42, Assert.Single(Enumerable.Return(42))); } diff --git a/Funcky.Test/Sequence/RepeatMaterializedTest.cs b/Funcky.Test/Sequence/RepeatMaterializedTest.cs index 45186d20e..a7356f34e 100644 --- a/Funcky.Test/Sequence/RepeatMaterializedTest.cs +++ b/Funcky.Test/Sequence/RepeatMaterializedTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -13,20 +11,20 @@ public sealed class RepeatMaterializedTest public void IsEnumeratedLazily() { var doNotEnumerate = new FailOnEnumerateReadOnlyCollection(Count: 0); - _ = Sequence.RepeatMaterialized(doNotEnumerate, 2); + _ = Enumerable.RepeatMaterialized(doNotEnumerate, 2); } [Property] public Property ARepeatedEmptySequenceIsStillEmpty(NonNegativeInt count) { - var repeated = Sequence.RepeatMaterialized(Array.Empty(), count.Get); + var repeated = Enumerable.RepeatMaterialized(Array.Empty(), count.Get); return (!repeated.Any()).ToProperty(); } [Property] public Property TheLengthOfTheGeneratedSequenceIsCorrect(List list, NonNegativeInt count) { - var repeatRange = Sequence.RepeatMaterialized(list, count.Get); + var repeatRange = Enumerable.RepeatMaterialized(list, count.Get); var materialized = repeatRange.ToList(); @@ -36,7 +34,7 @@ public Property TheLengthOfTheGeneratedSequenceIsCorrect(List list, NonNega [Property] public Property TheSequenceRepeatsTheGivenNumberOfTimes(List list, NonNegativeInt count) { - var repeatRange = Sequence.RepeatMaterialized(list, count.Get); + var repeatRange = Enumerable.RepeatMaterialized(list, count.Get); return repeatRange .IsSequenceRepeating(list) diff --git a/Funcky.Test/Sequence/RepeatRangeTest.cs b/Funcky.Test/Sequence/RepeatRangeTest.cs index 812ca5750..3d5cd0d62 100644 --- a/Funcky.Test/Sequence/RepeatRangeTest.cs +++ b/Funcky.Test/Sequence/RepeatRangeTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -14,7 +12,7 @@ public void RepeatRangeIsEnumeratedLazily() { var doNotEnumerate = new FailOnEnumerationSequence(); - using var repeatRange = Sequence.RepeatRange(doNotEnumerate, 2); + using var repeatRange = Enumerable.RepeatRange(doNotEnumerate, 2); } [Fact] @@ -22,14 +20,14 @@ public void RepeatRangeThrowsForNegativeCountBeforeEnumeration() { var doNotEnumerate = new FailOnEnumerationSequence(); - var exception = Assert.Throws(() => Sequence.RepeatRange(doNotEnumerate, -1).Dispose()); + var exception = Assert.Throws(() => Enumerable.RepeatRange(doNotEnumerate, -1).Dispose()); Assert.Equal("count", exception.ParamName); } [Fact] public void RepeatRangeWithCountZeroIsEmpty() { - using var repeatRange = Sequence.RepeatRange(Sequence.Return(1, 2, 3), 0); + using var repeatRange = Enumerable.RepeatRange(Enumerable.Return(1, 2, 3), 0); Assert.Empty(repeatRange); } @@ -37,7 +35,7 @@ public void RepeatRangeWithCountZeroIsEmpty() [Fact] public void RepeatRangeThrowsWhenAlreadyDisposed() { - var repeatRange = Sequence.RepeatRange(Sequence.Return(1337), 5); + var repeatRange = Enumerable.RepeatRange(Enumerable.Return(1337), 5); #pragma warning disable IDISP016 // we test behaviour after Dispose #pragma warning disable IDISP017 // we test behaviour after Dispose @@ -51,13 +49,13 @@ public void RepeatRangeThrowsWhenAlreadyDisposed() [Fact] public void RepeatRangeThrowsWhenAlreadyDisposedEvenIfYouDisposeBetweenMoveNext() { - var list = Sequence.Return(1337, 2, 5); + var list = Enumerable.Return(1337, 2, 5); const int repeats = 5; foreach (var i in Enumerable.Range(0, list.Count * repeats)) { - var repeatRange = Sequence.RepeatRange(list, repeats); + var repeatRange = Enumerable.RepeatRange(list, repeats); using var enumerator = repeatRange.GetEnumerator(); Assert.True(Enumerable.Range(0, i).All(_ => enumerator.MoveNext())); @@ -75,7 +73,7 @@ public void RepeatRangeThrowsWhenAlreadyDisposedEvenIfYouDisposeBetweenMoveNext( [Property] public Property TheLengthOfTheGeneratedRepeatRangeIsCorrect(List list, NonNegativeInt count) { - using var repeatRange = Sequence.RepeatRange(list, count.Get); + using var repeatRange = Enumerable.RepeatRange(list, count.Get); var materialized = repeatRange.ToList(); @@ -85,7 +83,7 @@ public Property TheLengthOfTheGeneratedRepeatRangeIsCorrect(List list, NonN [Property] public Property TheSequenceRepeatsTheGivenNumberOfTimes(List list, NonNegativeInt count) { - using var repeatRange = Sequence.RepeatRange(list, count.Get); + using var repeatRange = Enumerable.RepeatRange(list, count.Get); return repeatRange .IsSequenceRepeating(list) @@ -96,10 +94,10 @@ public Property TheSequenceRepeatsTheGivenNumberOfTimes(List list, NonNegat [Fact] public void RepeatRangeEnumeratesUnderlyingEnumerableOnlyOnce() { - var sequence = Sequence.Return("Test", "Hello", "Do", "Wait"); + var sequence = Enumerable.Return("Test", "Hello", "Do", "Wait"); var enumerateOnce = EnumerateOnce.Create(sequence); - using var repeatRange = Sequence.RepeatRange(enumerateOnce, 3); + using var repeatRange = Enumerable.RepeatRange(enumerateOnce, 3); repeatRange.ForEach(NoOperation); } diff --git a/Funcky.Test/Sequence/ReturnTest.cs b/Funcky.Test/Sequence/ReturnTest.cs index d1db091b7..2a34b922e 100644 --- a/Funcky.Test/Sequence/ReturnTest.cs +++ b/Funcky.Test/Sequence/ReturnTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -11,7 +9,7 @@ public sealed class ReturnTest [Property] public Property ReturnOfASingleItemElevatesThatItemIntoASingleItemedEnumerable(int item) { - var sequence = Sequence.Return(item); + var sequence = Enumerable.Return(item); return (sequence.SingleOrNone() == item).ToProperty(); } @@ -23,7 +21,7 @@ public void SequenceReturnCreatesAnEnumerableFromAnArbitraryNumberOfParameters() const string two = "Beta"; const string three = "Gamma"; - var sequence = Sequence.Return(one, two, three); + var sequence = Enumerable.Return(one, two, three); Assert.Collection( sequence, @@ -35,7 +33,7 @@ public void SequenceReturnCreatesAnEnumerableFromAnArbitraryNumberOfParameters() [Fact] public void TheSequenceCannotBeCastBackToAMutableArray() { - var sequence = Sequence.Return(1, 2, 3); + var sequence = Enumerable.Return(1, 2, 3); Assert.IsNotType(sequence); var list = Assert.IsAssignableFrom>(sequence); @@ -47,7 +45,7 @@ public void TheSequenceCannotBeCastBackToAMutableArray() public void TheSequenceIsAReadOnlyViewOverTheArrayPassedIn() { var array = new[] { 1, 2, 3 }; - var sequence = Sequence.Return(array); + var sequence = Enumerable.Return(array); array[0] = 42; @@ -60,7 +58,7 @@ public void PassingAnArrayBindsToTheParamsOverload() { var array = new[] { 1, 2, 3 }; - Assert.Equal(3, Sequence.Return(array).Count); - Assert.Single(Sequence.Return(array)); + Assert.Equal(3, Enumerable.Return(array).Count); + Assert.Single(Enumerable.Return(array)); } } diff --git a/Funcky.Test/Sequence/SuccessorsTest.cs b/Funcky.Test/Sequence/SuccessorsTest.cs index a22bfc3b0..1d1cb2788 100644 --- a/Funcky.Test/Sequence/SuccessorsTest.cs +++ b/Funcky.Test/Sequence/SuccessorsTest.cs @@ -1,5 +1,3 @@ -#pragma warning disable λ1202 // This test exercises the classic Sequence form itself. - namespace Funcky.Test; public sealed class SuccessorsTest @@ -7,13 +5,13 @@ public sealed class SuccessorsTest [Fact] public void ReturnsEmptySequenceWhenFirstItemIsNone() { - Assert.Empty(Sequence.Successors(Option.None, Identity)); + Assert.Empty(Enumerable.Successors(Option.None, Identity)); } [Fact] public void ReturnsOnlyTheFirstItemWhenSuccessorFunctionImmediatelyReturnsNone() { - var first = Assert.Single(Sequence.Successors(10, _ => Option.None)); + var first = Assert.Single(Enumerable.Successors(10, _ => Option.None)); Assert.Equal(10, first); } @@ -21,7 +19,7 @@ public void ReturnsOnlyTheFirstItemWhenSuccessorFunctionImmediatelyReturnsNone() public void SuccessorsWithNonOptionFunctionReturnsEndlessEnumerable() { const int count = 40; - Assert.Equal(count, Sequence.Successors(0, Identity).Take(count).Count()); + Assert.Equal(count, Enumerable.Successors(0, Identity).Take(count).Count()); } [Fact] @@ -29,7 +27,7 @@ public void SuccessorsReturnsEnumerableThatReturnsValuesBasedOnSeed() { Assert.Equal( Enumerable.Range(0, 10), - Sequence.Successors(0, i => i + 1).Take(10)); + Enumerable.Successors(0, i => i + 1).Take(10)); } [Fact] @@ -37,22 +35,22 @@ public void SuccessorsReturnsEnumerableThatReturnsItemUntilNoneIsReturnedFromFun { Assert.Equal( Enumerable.Range(0, 11), - Sequence.Successors(0, i => (i < 10).Then(i + 1))); + Enumerable.Successors(0, i => (i < 10).Then(i + 1))); } [Fact] public void CanGenerateFibonacciSequence() { - var fibonacci = Sequence.Successors((0, 1), n => (n.Item2, n.Item1 + n.Item2)).Select(n => n.Item1); - Assert.Equal(Sequence.Return(0, 1, 1, 2, 3, 5, 8, 13, 21, 34), fibonacci.Take(10)); + var fibonacci = Enumerable.Successors((0, 1), n => (n.Item2, n.Item1 + n.Item2)).Select(n => n.Item1); + Assert.Equal(Enumerable.Return(0, 1, 1, 2, 3, 5, 8, 13, 21, 34), fibonacci.Take(10)); } [Fact] public void ANullSuccessorFunctionIsRejectedWhenTheMethodIsCalled() { - Assert.Equal("successor", Assert.Throws(() => Sequence.Successors(1, (Func)null!)).ParamName); - Assert.Equal("successor", Assert.Throws(() => Sequence.Successors(1, (Func>)null!)).ParamName); - Assert.Equal("successor", Assert.Throws(() => Sequence.Successors(Option.Some(1), (Func)null!)).ParamName); - Assert.Equal("successor", Assert.Throws(() => Sequence.Successors(Option.Some(1), (Func>)null!)).ParamName); + Assert.Equal("successor", Assert.Throws(() => Enumerable.Successors(1, (Func)null!)).ParamName); + Assert.Equal("successor", Assert.Throws(() => Enumerable.Successors(1, (Func>)null!)).ParamName); + Assert.Equal("successor", Assert.Throws(() => Enumerable.Successors(Option.Some(1), (Func)null!)).ParamName); + Assert.Equal("successor", Assert.Throws(() => Enumerable.Successors(Option.Some(1), (Func>)null!)).ParamName); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs b/Funcky.Xunit.Test/AssertClass/ErrorTest.cs similarity index 58% rename from Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs rename to Funcky.Xunit.Test/AssertClass/ErrorTest.cs index 95f0f8c75..d958bfaca 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs +++ b/Funcky.Xunit.Test/AssertClass/ErrorTest.cs @@ -1,15 +1,13 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class ErrorTest { [Fact] public void DoesNotThrowForError() { - FunctionalAssert.Error(Result.Error(new ArgumentException("foo"))); + Assert.Error(Result.Error(new ArgumentException("foo"))); } [Fact] @@ -17,11 +15,11 @@ public void ThrowsForOk() { const string expectedMessage = """ - FunctionalAssert.Error() Failure: Values differ + Assert.Error() Failure: Values differ Expected: Error(...) Actual: Ok(42) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Error(Result.Ok(42))); + var exception = Assert.ThrowsAny(() => Assert.Error(Result.Ok(42))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -30,7 +28,7 @@ public void ReturnsTheErrorTypedWhenItIsOfTheExpectedType() { var exception = new ArgumentException("foo"); - var error = FunctionalAssert.Error(Result.Error(exception)); + var error = Assert.Error(Result.Error(exception)); Assert.Same(exception, error); } @@ -40,11 +38,11 @@ public void ThrowsForAnErrorOfAnotherType() { const string expectedMessage = """ - FunctionalAssert.Error() Failure: Values differ + Assert.Error() Failure: Values differ Expected: Error(ArgumentException) Actual: Error(InvalidOperationException) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Error(Result.Error(new InvalidOperationException("foo")))); + var exception = Assert.ThrowsAny(() => Assert.Error(Result.Error(new InvalidOperationException("foo")))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -53,11 +51,11 @@ public void ThrowsForOkWhenAnExceptionTypeIsExpected() { const string expectedMessage = """ - FunctionalAssert.Error() Failure: Values differ + Assert.Error() Failure: Values differ Expected: Error(ArgumentException) Actual: Ok(42) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Error(Result.Ok(42))); + var exception = Assert.ThrowsAny(() => Assert.Error(Result.Ok(42))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs b/Funcky.Xunit.Test/AssertClass/LeftTest.cs similarity index 57% rename from Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs rename to Funcky.Xunit.Test/AssertClass/LeftTest.cs index ee926b8d5..6eb718ca1 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs +++ b/Funcky.Xunit.Test/AssertClass/LeftTest.cs @@ -1,21 +1,19 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class LeftTest { [Fact] public void DoesNotThrowForMatchingLeft() { - FunctionalAssert.Left(10, Either.Left(10)); + Assert.Left(10, Either.Left(10)); } [Fact] public void DoesNotThrowForLeft() { - FunctionalAssert.Left(Either.Left(10)); + Assert.Left(Either.Left(10)); } [Fact] @@ -23,11 +21,11 @@ public void ThrowsForMismatchingLeft() { const string expectedMessage = """ - FunctionalAssert.Left() Failure: Values differ + Assert.Left() Failure: Values differ Expected: Left(10) Actual: Left(11) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Left(10, Either.Left(11))); + var exception = Assert.ThrowsAny(() => Assert.Left(10, Either.Left(11))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -36,11 +34,11 @@ public void ThrowsForRightWithExpectedValue() { const string expectedMessage = """ - FunctionalAssert.Left() Failure: Values differ + Assert.Left() Failure: Values differ Expected: Left(10) Actual: Right("right") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Left(10, Either.Right("right"))); + var exception = Assert.ThrowsAny(() => Assert.Left(10, Either.Right("right"))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -49,11 +47,11 @@ public void ThrowsForRight() { const string expectedMessage = """ - FunctionalAssert.Left() Failure: Values differ + Assert.Left() Failure: Values differ Expected: Left(...) Actual: Right("right") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Left(Either.Right("right"))); + var exception = Assert.ThrowsAny(() => Assert.Left(Either.Right("right"))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs b/Funcky.Xunit.Test/AssertClass/NoneTest.cs similarity index 52% rename from Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs rename to Funcky.Xunit.Test/AssertClass/NoneTest.cs index 7ba01706b..8ecbc8bdf 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs +++ b/Funcky.Xunit.Test/AssertClass/NoneTest.cs @@ -1,15 +1,13 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class NoneTest { [Fact] public void DoesNotThrowForNone() { - FunctionalAssert.None(Option.None); + Assert.None(Option.None); } [Fact] @@ -17,11 +15,11 @@ public void ThrowsForSome() { const string expectedMessage = """ - FunctionalAssert.None() Failure: Values differ + Assert.None() Failure: Values differ Expected: None Actual: Some("something") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.None(Option.Some("something"))); + var exception = Assert.ThrowsAny(() => Assert.None(Option.Some("something"))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs b/Funcky.Xunit.Test/AssertClass/OkTest.cs similarity index 58% rename from Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs rename to Funcky.Xunit.Test/AssertClass/OkTest.cs index 785cd503e..fe8257de8 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs +++ b/Funcky.Xunit.Test/AssertClass/OkTest.cs @@ -1,21 +1,19 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class OkTest { [Fact] public void DoesNotThrowForMatchingOk() { - FunctionalAssert.Ok(10, Result.Ok(10)); + Assert.Ok(10, Result.Ok(10)); } [Fact] public void DoesNotThrowForOk() { - FunctionalAssert.Ok(Result.Ok(10)); + Assert.Ok(Result.Ok(10)); } [Fact] @@ -23,11 +21,11 @@ public void ThrowsForMismatchingOk() { const string expectedMessage = """ - FunctionalAssert.Ok() Failure: Values differ + Assert.Ok() Failure: Values differ Expected: Ok(10) Actual: Ok(11) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Ok(10, Result.Ok(11))); + var exception = Assert.ThrowsAny(() => Assert.Ok(10, Result.Ok(11))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -36,11 +34,11 @@ public void ThrowsForErrorWithExpectedValue() { const string expectedMessage = """ - FunctionalAssert.Ok() Failure: Values differ + Assert.Ok() Failure: Values differ Expected: Ok(10) Actual: Error(System.ArgumentException: "message") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Ok(10, Result.Error(new ArgumentException("message")))); + var exception = Assert.ThrowsAny(() => Assert.Ok(10, Result.Error(new ArgumentException("message")))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -49,11 +47,11 @@ public void ThrowsForError() { const string expectedMessage = """ - FunctionalAssert.Ok() Failure: Values differ + Assert.Ok() Failure: Values differ Expected: Ok(...) Actual: Error(System.ArgumentException: "message") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Ok(Result.Error(new ArgumentException("message")))); + var exception = Assert.ThrowsAny(() => Assert.Ok(Result.Error(new ArgumentException("message")))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs b/Funcky.Xunit.Test/AssertClass/RightTest.cs similarity index 56% rename from Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs rename to Funcky.Xunit.Test/AssertClass/RightTest.cs index d4151abb9..c95796e01 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs +++ b/Funcky.Xunit.Test/AssertClass/RightTest.cs @@ -1,21 +1,19 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class RightTest { [Fact] public void DoesNotThrowForMatchingRight() { - FunctionalAssert.Right(10, Either.Right(10)); + Assert.Right(10, Either.Right(10)); } [Fact] public void DoesNotThrowForRight() { - FunctionalAssert.Right(Either.Right(10)); + Assert.Right(Either.Right(10)); } [Fact] @@ -23,11 +21,11 @@ public void ThrowsForMismatchingRight() { const string expectedMessage = """ - FunctionalAssert.Right() Failure: Values differ + Assert.Right() Failure: Values differ Expected: Right(10) Actual: Right(11) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Right(10, Either.Right(11))); + var exception = Assert.ThrowsAny(() => Assert.Right(10, Either.Right(11))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -36,11 +34,11 @@ public void ThrowsForLeftWithExpectedValue() { const string expectedMessage = """ - FunctionalAssert.Right() Failure: Values differ + Assert.Right() Failure: Values differ Expected: Right(10) Actual: Left("left") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Right(10, Either.Left("left"))); + var exception = Assert.ThrowsAny(() => Assert.Right(10, Either.Left("left"))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -49,11 +47,11 @@ public void ThrowsForLeft() { const string expectedMessage = """ - FunctionalAssert.Right() Failure: Values differ + Assert.Right() Failure: Values differ Expected: Right(...) Actual: Left("left") """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Right(Either.Left("left"))); + var exception = Assert.ThrowsAny(() => Assert.Right(Either.Left("left"))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs b/Funcky.Xunit.Test/AssertClass/SomeTest.cs similarity index 58% rename from Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs rename to Funcky.Xunit.Test/AssertClass/SomeTest.cs index 0705bb3d6..b467e0909 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs +++ b/Funcky.Xunit.Test/AssertClass/SomeTest.cs @@ -1,21 +1,19 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class SomeTest { [Fact] public void DoesNotThrowForMatchingSome() { - FunctionalAssert.Some(10, Option.Some(10)); + Assert.Some(10, Option.Some(10)); } [Fact] public void DoesNotThrowForRight() { - _ = FunctionalAssert.Some(Option.Some(10)); + _ = Assert.Some(Option.Some(10)); } [Fact] @@ -23,11 +21,11 @@ public void ThrowsForMismatchingSome() { const string expectedMessage = """ - FunctionalAssert.Some() Failure: Values differ + Assert.Some() Failure: Values differ Expected: Some(10) Actual: Some(11) """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Some(10, Option.Some(11))); + var exception = Assert.ThrowsAny(() => Assert.Some(10, Option.Some(11))); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -36,11 +34,11 @@ public void ThrowsForNoneWithExpectedValue() { const string expectedMessage = """ - FunctionalAssert.Some() Failure: Values differ + Assert.Some() Failure: Values differ Expected: Some(10) Actual: None """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Some(10, Option.None)); + var exception = Assert.ThrowsAny(() => Assert.Some(10, Option.None)); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } @@ -49,11 +47,11 @@ public void ThrowsForNone() { const string expectedMessage = """ - FunctionalAssert.Some() Failure: Values differ + Assert.Some() Failure: Values differ Expected: Some(...) Actual: None """; - var exception = Assert.ThrowsAny(() => FunctionalAssert.Some(Option.None)); + var exception = Assert.ThrowsAny(() => Assert.Some(Option.None)); Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); } } diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/StackTraceTest.cs b/Funcky.Xunit.Test/AssertClass/StackTraceTest.cs similarity index 59% rename from Funcky.Xunit.Test/FunctionalAssertClass/StackTraceTest.cs rename to Funcky.Xunit.Test/AssertClass/StackTraceTest.cs index 80869da1c..af47f7afd 100644 --- a/Funcky.Xunit.Test/FunctionalAssertClass/StackTraceTest.cs +++ b/Funcky.Xunit.Test/AssertClass/StackTraceTest.cs @@ -1,8 +1,6 @@ -#pragma warning disable λ1203 // This test exercises the classic FunctionalAssert form itself. - using Xunit.Sdk; -namespace Funcky.Xunit.Test.FunctionalAssertClass; +namespace Funcky.Xunit.Test.AssertClass; public sealed class StackTraceTest { @@ -19,16 +17,16 @@ public void StackTraceOfFailedAssertionDoesNotContainTheMonadOrLambdaFrames() public void StackTraceOfFailedAssertionStartsAtTheTestMethod() { var firstLine = CaptureStackTrace().Split('\n')[0]; - Assert.DoesNotContain("Funcky.FunctionalAssert.", firstLine); + Assert.DoesNotContain("Funcky.Extensions.AssertExtensions.", firstLine); Assert.Contains(nameof(CaptureStackTrace), firstLine); } #else - // Frameworks without [StackTraceHidden] cannot hide the FunctionalAssert frame; it is the only frame above the test code. + // Frameworks without [StackTraceHidden] cannot hide the assertion frame; it is the only frame above the test code. [Fact] - public void StackTraceOfFailedAssertionStartsAtTheFunctionalAssertMethod() + public void StackTraceOfFailedAssertionStartsAtTheAssertionMethod() { var lines = CaptureStackTrace().Split('\n'); - Assert.Contains("Funcky.FunctionalAssert.Some", lines[0]); + Assert.Contains("Funcky.Extensions.AssertExtensions.Some", lines[0]); Assert.Contains(nameof(CaptureStackTrace), lines[1]); } #endif @@ -37,13 +35,13 @@ private static string CaptureStackTrace() { try { - FunctionalAssert.Some(Option.None); + Assert.Some(Option.None); } catch (XunitException exception) { return exception.StackTrace ?? string.Empty; } - throw new InvalidOperationException("FunctionalAssert.Some did not throw"); + throw new InvalidOperationException("Assert.Some did not throw"); } } diff --git a/Funcky.Xunit.Test/Extensions/AssertExtensionsTest.cs b/Funcky.Xunit.Test/Extensions/AssertExtensionsTest.cs index 8c9d0c7e3..2b5380a03 100644 --- a/Funcky.Xunit.Test/Extensions/AssertExtensionsTest.cs +++ b/Funcky.Xunit.Test/Extensions/AssertExtensionsTest.cs @@ -17,7 +17,7 @@ public void SomeThrowsForNone() { const string expectedMessage = """ - FunctionalAssert.Some() Failure: Values differ + Assert.Some() Failure: Values differ Expected: Some(10) Actual: None """; @@ -37,7 +37,7 @@ public void NoneThrowsForSome() { const string expectedMessage = """ - FunctionalAssert.None() Failure: Values differ + Assert.None() Failure: Values differ Expected: None Actual: Some(10) """; @@ -58,7 +58,7 @@ public void OkThrowsForError() { const string expectedMessage = """ - FunctionalAssert.Ok() Failure: Values differ + Assert.Ok() Failure: Values differ Expected: Ok(10) Actual: Error(System.InvalidOperationException: "boom") """; @@ -80,7 +80,7 @@ public void ErrorThrowsForOk() { const string expectedMessage = """ - FunctionalAssert.Error() Failure: Values differ + Assert.Error() Failure: Values differ Expected: Error(...) Actual: Ok(10) """; @@ -108,7 +108,7 @@ public void LeftThrowsForRight() { const string expectedMessage = """ - FunctionalAssert.Left() Failure: Values differ + Assert.Left() Failure: Values differ Expected: Left("failure") Actual: Right(42) """; @@ -130,7 +130,7 @@ public void RightThrowsForLeft() { const string expectedMessage = """ - FunctionalAssert.Right() Failure: Values differ + Assert.Right() Failure: Values differ Expected: Right(42) Actual: Left("failure") """; @@ -144,7 +144,6 @@ public void RightThrowsForLeft() public void StackTraceOfFailedAssertionStartsAtTheTestMethod() { var firstLine = CaptureStackTrace().Split('\n')[0]; - Assert.DoesNotContain("Funcky.FunctionalAssert.", firstLine); Assert.DoesNotContain("Funcky.Extensions.AssertExtensions.", firstLine); Assert.Contains(nameof(CaptureStackTrace), firstLine); } diff --git a/Funcky.Xunit.v3/Funcky.Xunit.v3.csproj b/Funcky.Xunit.v3/Funcky.Xunit.v3.csproj index d1ce65fab..1e36c7b44 100644 --- a/Funcky.Xunit.v3/Funcky.Xunit.v3.csproj +++ b/Funcky.Xunit.v3/Funcky.Xunit.v3.csproj @@ -8,7 +8,7 @@ false Package to use Funcky with xUnit v3 Functional Monad xUnit - 1.1.0 + 2.0.0 true snupkg Funcky diff --git a/Funcky.Xunit.v3/PublicAPI/all/PublicAPI.Unshipped.txt b/Funcky.Xunit.v3/PublicAPI/all/PublicAPI.Unshipped.txt index 7dc5c5811..c9c5f16e8 100644 --- a/Funcky.Xunit.v3/PublicAPI/all/PublicAPI.Unshipped.txt +++ b/Funcky.Xunit.v3/PublicAPI/all/PublicAPI.Unshipped.txt @@ -1 +1,13 @@ #nullable enable +*REMOVED*Funcky.FunctionalAssert +*REMOVED*static Funcky.FunctionalAssert.Error(Funcky.Monads.Result result) -> System.Exception! +*REMOVED*static Funcky.FunctionalAssert.Left(Funcky.Monads.Either either) -> TLeft +*REMOVED*static Funcky.FunctionalAssert.Left(TLeft expectedLeft, Funcky.Monads.Either either) -> void +*REMOVED*static Funcky.FunctionalAssert.None(Funcky.Monads.Option option) -> void +*REMOVED*static Funcky.FunctionalAssert.Ok(Funcky.Monads.Result result) -> TValidResult +*REMOVED*static Funcky.FunctionalAssert.Ok(TValidResult expectedResult, Funcky.Monads.Result result) -> void +*REMOVED*static Funcky.FunctionalAssert.Right(Funcky.Monads.Either either) -> TRight +*REMOVED*static Funcky.FunctionalAssert.Right(TRight expectedRight, Funcky.Monads.Either either) -> void +*REMOVED*static Funcky.FunctionalAssert.Some(Funcky.Monads.Option option) -> TItem +*REMOVED*static Funcky.FunctionalAssert.Some(TItem expectedValue, Funcky.Monads.Option option) -> void +*REMOVED*static Funcky.FunctionalAssert.Error(Funcky.Monads.Result result) -> TException! diff --git a/Funcky.Xunit.v3/package.md b/Funcky.Xunit.v3/package.md index 965f92e02..f756a225c 100644 --- a/Funcky.Xunit.v3/package.md +++ b/Funcky.Xunit.v3/package.md @@ -5,7 +5,10 @@ It also enables `Option`, `Either` and `Unit` for use in theory data by providin custom [serializers]. ## Main Types -* `Funcky.FunctionalAssert`—Assertions for Funcky's monads, also available as `Assert.Some`, `Assert.Ok`, … on xunit's `Assert` (C# 14). +* `Funcky.Extensions.AssertExtensions`—Assertions for Funcky's monads as static extension members of xunit's `Assert`: `Assert.Some`, `Assert.None`, `Assert.Ok`, `Assert.Error`, `Assert.Left`, `Assert.Right`. + +The assertions are C# 14 extension members, so they need the .NET 10 SDK or newer; `using Funcky.Extensions;` brings +them into scope. ## How to Use @@ -15,26 +18,24 @@ var some = Option.Some("hello world"); var none = Option.None; // Asserts that an Option contains a value, returns its value. -var text = FunctionalAssert.Some(some); +var text = Assert.Some(some); Assert.Equal("hello world", text); // Asserts that an Option contains the given value. -FunctionalAssert.Some("hello world", some); +Assert.Some("hello world", some); // Asserts that the Option is empty. -FunctionalAssert.None(none); +Assert.None(none); ``` -### Use the assertions on `Assert` -With the C# 14 compiler the same assertions are static extension members of xunit's `Assert`: +### Test a Result ```csharp -var text = Assert.Some(some); -Assert.Some("hello world", some); -Assert.None(none); +// Asserts that the Result is Ok, returns the value. var value = Assert.Ok(result); + +// Asserts that the Result is an error, returns the exception. var error = Assert.Error(result); -var left = Assert.Left(either); -var right = Assert.Right(either); +var typed = Assert.Error(result); ``` ### Test an Either @@ -43,18 +44,18 @@ var right = Either.Return(42); var left = Either.Left("failure"); // Asserts that the Either is a right, returns the value. -var value = FunctionalAssert.Right(right); +var value = Assert.Right(right); Assert.Equal(42, value); // Asserts that the Either is a right with the given value. -FunctionalAssert.Right(42, right); +Assert.Right(42, right); // Asserts that the Either is a left, returns the value. -var message = FunctionalAssert.Left(left); +var message = Assert.Left(left); Assert.Equal("failure", message); // Asserts that the Either is a left with the given value. -FunctionalAssert.Left("failure", left); +Assert.Left("failure", left); ``` ## Use an Option as theory data diff --git a/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs b/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs index 641839ba8..dd5d80707 100644 --- a/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs +++ b/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs @@ -1,6 +1,6 @@ namespace Funcky.CodeAnalysis; -/// Marks FunctionalAssert methods that have an accompanying overload that -/// takes the expected value. +/// Marks the assertions on Assert that have an accompanying overload which +/// takes the expected value; the analyzer rule λ1101 prefers that overload. [AttributeUsage(AttributeTargets.Method)] internal sealed class AssertMethodHasOverloadWithExpectedValueAttribute : Attribute; diff --git a/Funcky.Xunit/Extensions/AssertExtensions.cs b/Funcky.Xunit/Extensions/AssertExtensions.cs index 9b8e2e063..af334e64b 100644 --- a/Funcky.Xunit/Extensions/AssertExtensions.cs +++ b/Funcky.Xunit/Extensions/AssertExtensions.cs @@ -7,8 +7,7 @@ namespace Funcky.Extensions; /// Assertions for Funcky's monads as static extension members of xunit's , -/// so they are found where the other assertions live: Assert.Some(option). -/// offers the same assertions as plain static methods for compilers older than C# 14. +/// so they are found where the other assertions live: Assert.Some(option). public static class AssertExtensions { extension(Assert) diff --git a/Funcky.Xunit/Funcky.Xunit.csproj b/Funcky.Xunit/Funcky.Xunit.csproj index 121e43467..b6a88fa94 100644 --- a/Funcky.Xunit/Funcky.Xunit.csproj +++ b/Funcky.Xunit/Funcky.Xunit.csproj @@ -8,7 +8,7 @@ false Package to use Funcky with xUnit Functional Monad xUnit - 2.3.0 + 3.0.0 true snupkg Funcky diff --git a/Funcky.Xunit/FunctionalAssert/Error.cs b/Funcky.Xunit/FunctionalAssert/Error.cs deleted file mode 100644 index 4a5976207..000000000 --- a/Funcky.Xunit/FunctionalAssert/Error.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Xunit; -using Xunit.Sdk; - -namespace Funcky; - -public static partial class FunctionalAssert -{ - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static Exception Error(Result result) - where TValidResult : notnull - { - try - { - return Assert.Error(result); - } - catch (XunitException exception) - { - throw exception; - } - } - - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static TException Error(Result result) - where TException : Exception - where TValidResult : notnull - { - try - { - return Assert.Error(result); - } - catch (XunitException exception) - { - throw exception; - } - } -} diff --git a/Funcky.Xunit/FunctionalAssert/Left.cs b/Funcky.Xunit/FunctionalAssert/Left.cs deleted file mode 100644 index 381af468c..000000000 --- a/Funcky.Xunit/FunctionalAssert/Left.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Funcky.CodeAnalysis; -using Xunit; -using Xunit.Sdk; - -namespace Funcky; - -public static partial class FunctionalAssert -{ - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static void Left(TLeft expectedLeft, Either either) - where TLeft : notnull - where TRight : notnull - { - try - { - Assert.Left(expectedLeft, either); - } - catch (XunitException exception) - { - throw exception; - } - } - - /// - [AssertMethodHasOverloadWithExpectedValue] - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static TLeft Left(Either either) - where TLeft : notnull - where TRight : notnull - { - try - { - return Assert.Left(either); - } - catch (XunitException exception) - { - throw exception; - } - } -} diff --git a/Funcky.Xunit/FunctionalAssert/None.cs b/Funcky.Xunit/FunctionalAssert/None.cs deleted file mode 100644 index 172161477..000000000 --- a/Funcky.Xunit/FunctionalAssert/None.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Xunit; -using Xunit.Sdk; - -namespace Funcky; - -public static partial class FunctionalAssert -{ - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static void None(Option option) - where TItem : notnull - { - try - { - Assert.None(option); - } - catch (XunitException exception) - { - throw exception; - } - } -} diff --git a/Funcky.Xunit/FunctionalAssert/Ok.cs b/Funcky.Xunit/FunctionalAssert/Ok.cs deleted file mode 100644 index 4e2e49d89..000000000 --- a/Funcky.Xunit/FunctionalAssert/Ok.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Funcky.CodeAnalysis; -using Xunit; -using Xunit.Sdk; - -namespace Funcky; - -public static partial class FunctionalAssert -{ - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static void Ok(TValidResult expectedResult, Result result) - where TValidResult : notnull - { - try - { - Assert.Ok(expectedResult, result); - } - catch (XunitException exception) - { - throw exception; - } - } - - /// - [AssertMethodHasOverloadWithExpectedValue] - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static TValidResult Ok(Result result) - where TValidResult : notnull - { - try - { - return Assert.Ok(result); - } - catch (XunitException exception) - { - throw exception; - } - } -} diff --git a/Funcky.Xunit/FunctionalAssert/Right.cs b/Funcky.Xunit/FunctionalAssert/Right.cs deleted file mode 100644 index 0c8417b59..000000000 --- a/Funcky.Xunit/FunctionalAssert/Right.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Funcky.CodeAnalysis; -using Xunit; -using Xunit.Sdk; - -namespace Funcky; - -public static partial class FunctionalAssert -{ - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static void Right(TRight expectedRight, Either either) - where TLeft : notnull - where TRight : notnull - { - try - { - Assert.Right(expectedRight, either); - } - catch (XunitException exception) - { - throw exception; - } - } - - /// - [AssertMethodHasOverloadWithExpectedValue] - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static TRight Right(Either either) - where TLeft : notnull - where TRight : notnull - { - try - { - return Assert.Right(either); - } - catch (XunitException exception) - { - throw exception; - } - } -} diff --git a/Funcky.Xunit/FunctionalAssert/Some.cs b/Funcky.Xunit/FunctionalAssert/Some.cs deleted file mode 100644 index 03a28ca22..000000000 --- a/Funcky.Xunit/FunctionalAssert/Some.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Funcky.CodeAnalysis; -using Xunit; -using Xunit.Sdk; - -namespace Funcky; - -public static partial class FunctionalAssert -{ - /// - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static void Some(TItem expectedValue, Option option) - where TItem : notnull - { - try - { - Assert.Some(expectedValue, option); - } - catch (XunitException exception) - { - throw exception; - } - } - - /// - [AssertMethodHasOverloadWithExpectedValue] - [Pure] - #if STACK_TRACE_HIDDEN_SUPPORTED - [System.Diagnostics.StackTraceHidden] - #endif - [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] - [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] - public static TItem Some(Option option) - where TItem : notnull - { - try - { - return Assert.Some(option); - } - catch (XunitException exception) - { - throw exception; - } - } -} diff --git a/Funcky.Xunit/FunctionalAssertException.cs b/Funcky.Xunit/FunctionalAssertException.cs index 79afb1ab8..1101488ef 100644 --- a/Funcky.Xunit/FunctionalAssertException.cs +++ b/Funcky.Xunit/FunctionalAssertException.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Xunit; using Xunit.Sdk; namespace Funcky; @@ -13,8 +14,8 @@ public static XunitException ForMismatchedValues( [CallerMemberName] string? assertionName = null) { var assertionLabel = assertionName is not null - ? $"{nameof(FunctionalAssert)}.{assertionName}()" - : nameof(FunctionalAssert); + ? $"{nameof(Assert)}.{assertionName}()" + : nameof(Assert); return new XunitException( $"{assertionLabel} Failure: Values differ{Environment.NewLine}" + $"Expected: {expected.Replace(Environment.NewLine, NewLineAndIndent)}{Environment.NewLine}" diff --git a/Funcky.Xunit/PublicAPI/all/PublicAPI.Unshipped.txt b/Funcky.Xunit/PublicAPI/all/PublicAPI.Unshipped.txt index 7dc5c5811..da60d714e 100644 --- a/Funcky.Xunit/PublicAPI/all/PublicAPI.Unshipped.txt +++ b/Funcky.Xunit/PublicAPI/all/PublicAPI.Unshipped.txt @@ -1 +1,13 @@ #nullable enable +*REMOVED*static Funcky.FunctionalAssert.Some(TItem expectedValue, Funcky.Monads.Option option) -> void +*REMOVED*static Funcky.FunctionalAssert.Some(Funcky.Monads.Option option) -> TItem +*REMOVED*static Funcky.FunctionalAssert.Right(TRight expectedRight, Funcky.Monads.Either either) -> void +*REMOVED*static Funcky.FunctionalAssert.Right(Funcky.Monads.Either either) -> TRight +*REMOVED*static Funcky.FunctionalAssert.Ok(TValidResult expectedResult, Funcky.Monads.Result result) -> void +*REMOVED*static Funcky.FunctionalAssert.Ok(Funcky.Monads.Result result) -> TValidResult +*REMOVED*static Funcky.FunctionalAssert.None(Funcky.Monads.Option option) -> void +*REMOVED*static Funcky.FunctionalAssert.Left(TLeft expectedLeft, Funcky.Monads.Either either) -> void +*REMOVED*static Funcky.FunctionalAssert.Left(Funcky.Monads.Either either) -> TLeft +*REMOVED*static Funcky.FunctionalAssert.Error(Funcky.Monads.Result result) -> System.Exception! +*REMOVED*Funcky.FunctionalAssert +*REMOVED*static Funcky.FunctionalAssert.Error(Funcky.Monads.Result result) -> TException! diff --git a/Funcky.Xunit/package.md b/Funcky.Xunit/package.md index 412ce8613..9b121ed91 100644 --- a/Funcky.Xunit/package.md +++ b/Funcky.Xunit/package.md @@ -2,9 +2,12 @@ Provides expressive assertions for xUnit.net 2.x that unpack [Funcky]'s monads. ## Main Types -* `Funcky.FunctionalAssert`—Assertions for Funcky's monads, also available as `Assert.Some`, `Assert.Ok`, … on xunit's `Assert` (C# 14). +* `Funcky.Extensions.AssertExtensions`—Assertions for Funcky's monads as static extension members of xunit's `Assert`: `Assert.Some`, `Assert.None`, `Assert.Ok`, `Assert.Error`, `Assert.Left`, `Assert.Right`. * `Funcky.Extensions.ToTheoryDataExtension`—Conveniently convert `IEnumerable`s to `TheoryData<..>`. +The assertions are C# 14 extension members, so they need the .NET 10 SDK or newer; `using Funcky.Extensions;` brings +them into scope. + ## How to Use ### Test an Option @@ -13,26 +16,24 @@ var some = Option.Some("hello world"); var none = Option.None; // Asserts that an Option contains a value, returns its value. -var text = FunctionalAssert.Some(some); +var text = Assert.Some(some); Assert.Equal("hello world", text); // Asserts that an Option contains the given value. -FunctionalAssert.Some("hello world", some); +Assert.Some("hello world", some); // Asserts that the Option is empty. -FunctionalAssert.None(none); +Assert.None(none); ``` -### Use the assertions on `Assert` -With the C# 14 compiler the same assertions are static extension members of xunit's `Assert`: +### Test a Result ```csharp -var text = Assert.Some(some); -Assert.Some("hello world", some); -Assert.None(none); +// Asserts that the Result is Ok, returns the value. var value = Assert.Ok(result); + +// Asserts that the Result is an error, returns the exception. var error = Assert.Error(result); -var left = Assert.Left(either); -var right = Assert.Right(either); +var typed = Assert.Error(result); ``` ### Test an Either @@ -41,18 +42,18 @@ var right = Either.Return(42); var left = Either.Left("failure"); // Asserts that the Either is a right, returns the value. -var value = FunctionalAssert.Right(right); +var value = Assert.Right(right); Assert.Equal(42, value); // Asserts that the Either is a right with the given value. -FunctionalAssert.Right(42, right); +Assert.Right(42, right); // Asserts that the Either is a left, returns the value. -var message = FunctionalAssert.Left(left); +var message = Assert.Left(left); Assert.Equal("failure", message); // Asserts that the Either is a left with the given value. -FunctionalAssert.Left("failure", left); +Assert.Left("failure", left); ``` ### Create theory data from an `IEnumerable` diff --git a/Funcky.Xunit/readme.md b/Funcky.Xunit/readme.md index 6323c8222..07d443a2f 100644 --- a/Funcky.Xunit/readme.md +++ b/Funcky.Xunit/readme.md @@ -1,13 +1,13 @@ # Funcky.Xunit ### Stack Trace -When a `FunctionalAssert` method fails, the stack trace of the exception should start at the test method, just like the -exceptions thrown by xUnit's own `Assert`. Every `FunctionalAssert` method therefore catches the assertion exception and -re-throws it, which resets the stack trace to the `FunctionalAssert` method and removes the frames of the monad and the -lambda that actually threw. +When an assertion of `AssertExtensions` (`Assert.Some`, `Assert.Ok`, …) fails, the stack trace of the exception should +start at the test method, just like the exceptions thrown by xUnit's own `Assert`. Every assertion therefore catches +the assertion exception and re-throws it, which resets the stack trace to the assertion method and removes the frames +of the monad and the lambda that actually threw. -On .NET 6 and later the methods are additionally marked with `[StackTraceHidden]`, so the `FunctionalAssert` frame itself -is hidden as well and the trace starts at the test method. +On .NET 6 and later the methods are additionally marked with `[StackTraceHidden]`, so the assertion's own frame is +hidden as well and the trace starts at the test method. On older frameworks (the `netstandard2.0` asset) there is no way to hide a frame, so the trace starts with one -`FunctionalAssert` frame followed by the test method. `AggressiveInlining` does not help: the JIT never inlines a method +`AssertExtensions` frame followed by the test method. `AggressiveInlining` does not help: the JIT never inlines a method that contains an exception handler. diff --git a/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 5f1654d83..de892879b 100644 --- a/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -122,3 +122,19 @@ *REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void +*REMOVED*Funcky.Sequence +*REMOVED*static Funcky.Sequence.Concat(System.Collections.Generic.IEnumerable!>! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Concat(params System.Collections.Generic.IEnumerable![]! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Cycle(TResult element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleMaterialized(System.Collections.Generic.IReadOnlyCollection! source) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleRange(System.Collections.Generic.IEnumerable! source) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatMaterialized(System.Collections.Generic.IReadOnlyCollection! source, int count) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatRange(System.Collections.Generic.IEnumerable! source, int count) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.Return(TResult element) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Return(params TResult[]! elements) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func! successor) -> System.Collections.Generic.IEnumerable! diff --git a/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt index 7172b081f..e9bc78bfb 100644 --- a/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt @@ -168,3 +168,19 @@ static Funcky.Extensions.StringExtensions.LastIndexOfOrNone(this string! haystac *REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void +*REMOVED*Funcky.Sequence +*REMOVED*static Funcky.Sequence.Concat(System.Collections.Generic.IEnumerable!>! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Concat(params System.Collections.Generic.IEnumerable![]! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Cycle(TResult element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleMaterialized(System.Collections.Generic.IReadOnlyCollection! source) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleRange(System.Collections.Generic.IEnumerable! source) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatMaterialized(System.Collections.Generic.IReadOnlyCollection! source, int count) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatRange(System.Collections.Generic.IEnumerable! source, int count) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.Return(TResult element) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Return(params TResult[]! elements) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func! successor) -> System.Collections.Generic.IEnumerable! diff --git a/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 3c6842c20..294dfdb0a 100644 --- a/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -6,3 +6,19 @@ *REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void +*REMOVED*Funcky.Sequence +*REMOVED*static Funcky.Sequence.Concat(System.Collections.Generic.IEnumerable!>! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Concat(params System.Collections.Generic.IEnumerable![]! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Cycle(TResult element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleMaterialized(System.Collections.Generic.IReadOnlyCollection! source) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleRange(System.Collections.Generic.IEnumerable! source) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatMaterialized(System.Collections.Generic.IReadOnlyCollection! source, int count) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatRange(System.Collections.Generic.IEnumerable! source, int count) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.Return(TResult element) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Return(params TResult[]! elements) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func! successor) -> System.Collections.Generic.IEnumerable! diff --git a/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 3c6842c20..294dfdb0a 100644 --- a/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -6,3 +6,19 @@ *REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void +*REMOVED*Funcky.Sequence +*REMOVED*static Funcky.Sequence.Concat(System.Collections.Generic.IEnumerable!>! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Concat(params System.Collections.Generic.IEnumerable![]! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Cycle(TResult element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleMaterialized(System.Collections.Generic.IReadOnlyCollection! source) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleRange(System.Collections.Generic.IEnumerable! source) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatMaterialized(System.Collections.Generic.IReadOnlyCollection! source, int count) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatRange(System.Collections.Generic.IEnumerable! source, int count) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.Return(TResult element) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Return(params TResult[]! elements) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func! successor) -> System.Collections.Generic.IEnumerable! diff --git a/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 3c6842c20..294dfdb0a 100644 --- a/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -6,3 +6,19 @@ *REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void +*REMOVED*Funcky.Sequence +*REMOVED*static Funcky.Sequence.Concat(System.Collections.Generic.IEnumerable!>! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Concat(params System.Collections.Generic.IEnumerable![]! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Cycle(TResult element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleMaterialized(System.Collections.Generic.IReadOnlyCollection! source) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleRange(System.Collections.Generic.IEnumerable! source) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatMaterialized(System.Collections.Generic.IReadOnlyCollection! source, int count) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatRange(System.Collections.Generic.IEnumerable! source, int count) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.Return(TResult element) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Return(params TResult[]! elements) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func! successor) -> System.Collections.Generic.IEnumerable! diff --git a/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 3c6842c20..294dfdb0a 100644 --- a/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -6,3 +6,19 @@ *REMOVED*Funcky.Monads.Result.Switch(System.Action! ok, System.Action! error) -> void Funcky.Monads.Result.Match(System.Func! error, System.Func! ok) -> TMatchResult Funcky.Monads.Result.Switch(System.Action! error, System.Action! ok) -> void +*REMOVED*Funcky.Sequence +*REMOVED*static Funcky.Sequence.Concat(System.Collections.Generic.IEnumerable!>! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Concat(params System.Collections.Generic.IEnumerable![]! sources) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Cycle(TResult element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleMaterialized(System.Collections.Generic.IReadOnlyCollection! source) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.CycleRange(System.Collections.Generic.IEnumerable! source) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.FromNullable(TResult? element) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatMaterialized(System.Collections.Generic.IReadOnlyCollection! source, int count) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.RepeatRange(System.Collections.Generic.IEnumerable! source, int count) -> Funcky.IBuffer! +*REMOVED*static Funcky.Sequence.Return(TResult element) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Return(params TResult[]! elements) -> System.Collections.Generic.IReadOnlyList! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(Funcky.Monads.Option first, System.Func! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func>! successor) -> System.Collections.Generic.IEnumerable! +*REMOVED*static Funcky.Sequence.Successors(TResult first, System.Func! successor) -> System.Collections.Generic.IEnumerable! diff --git a/Funcky/Sequence/Sequence.Concat.cs b/Funcky/Sequence/Sequence.Concat.cs deleted file mode 100644 index 307b8888d..000000000 --- a/Funcky/Sequence/Sequence.Concat.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IEnumerable Concat(params IEnumerable[] sources) - => Enumerable.Concat(sources.AsEnumerable()); - - /// - [Pure] - public static IEnumerable Concat(IEnumerable> sources) - => Enumerable.Concat(sources); -} diff --git a/Funcky/Sequence/Sequence.Cycle.cs b/Funcky/Sequence/Sequence.Cycle.cs deleted file mode 100644 index a27d5267e..000000000 --- a/Funcky/Sequence/Sequence.Cycle.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IEnumerable Cycle(TResult element) - => Enumerable.Cycle(element); -} diff --git a/Funcky/Sequence/Sequence.CycleMaterialized.cs b/Funcky/Sequence/Sequence.CycleMaterialized.cs deleted file mode 100644 index b6d230fb3..000000000 --- a/Funcky/Sequence/Sequence.CycleMaterialized.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IEnumerable CycleMaterialized(IReadOnlyCollection source) - => Enumerable.CycleMaterialized(source); -} diff --git a/Funcky/Sequence/Sequence.CycleRange.cs b/Funcky/Sequence/Sequence.CycleRange.cs deleted file mode 100644 index ce1b0dbea..000000000 --- a/Funcky/Sequence/Sequence.CycleRange.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IBuffer CycleRange(IEnumerable source) - => Enumerable.CycleRange(source); -} diff --git a/Funcky/Sequence/Sequence.FromNullable.cs b/Funcky/Sequence/Sequence.FromNullable.cs deleted file mode 100644 index 41ec11174..000000000 --- a/Funcky/Sequence/Sequence.FromNullable.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IEnumerable FromNullable(TResult? element) - where TResult : class - => Enumerable.FromNullable(element); - - /// - [Pure] - public static IEnumerable FromNullable(TResult? element) - where TResult : struct - => Enumerable.FromNullable(element); -} diff --git a/Funcky/Sequence/Sequence.RepeatMaterialized.cs b/Funcky/Sequence/Sequence.RepeatMaterialized.cs deleted file mode 100644 index 28edba8ff..000000000 --- a/Funcky/Sequence/Sequence.RepeatMaterialized.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IEnumerable RepeatMaterialized(IReadOnlyCollection source, int count) - => Enumerable.RepeatMaterialized(source, count); -} diff --git a/Funcky/Sequence/Sequence.RepeatRange.cs b/Funcky/Sequence/Sequence.RepeatRange.cs deleted file mode 100644 index 45f974b4b..000000000 --- a/Funcky/Sequence/Sequence.RepeatRange.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IBuffer RepeatRange(IEnumerable source, int count) - => Enumerable.RepeatRange(source, count); -} diff --git a/Funcky/Sequence/Sequence.Return.cs b/Funcky/Sequence/Sequence.Return.cs deleted file mode 100644 index ad62e896f..000000000 --- a/Funcky/Sequence/Sequence.Return.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IReadOnlyList Return(TResult element) - => Enumerable.Return(element); - - /// - [Pure] - public static IReadOnlyList Return(params TResult[] elements) - => Enumerable.Return(elements); -} diff --git a/Funcky/Sequence/Sequence.Successors.cs b/Funcky/Sequence/Sequence.Successors.cs deleted file mode 100644 index 411014eb7..000000000 --- a/Funcky/Sequence/Sequence.Successors.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Funcky; - -public static partial class Sequence -{ - /// - [Pure] - public static IEnumerable Successors(Option first, Func> successor) - where TResult : notnull - => Enumerable.Successors(first, successor); - - /// - [Pure] - public static IEnumerable Successors(TResult first, Func> successor) - where TResult : notnull - => Enumerable.Successors(first, successor); - - /// - [Pure] - public static IEnumerable Successors(Option first, Func successor) - where TResult : notnull - => Enumerable.Successors(first, successor); - - /// - [Pure] - public static IEnumerable Successors(TResult first, Func successor) - => Enumerable.Successors(first, successor); -} diff --git a/README.md b/README.md index 4334fa53a..2d91a5758 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ types to **Funcky**, so a project can drop the reference once it targets .NET 10 | Package | NuGet | Description | | --- | --- | --- | -| **Funcky.Xunit** | [![NuGet Version](https://img.shields.io/nuget/v/Funcky.Xunit)](https://www.nuget.org/packages/Funcky.Xunit) | `FunctionalAssert` for xUnit.net 2.x: unpack `Option`, `Result` and `Either` in tests. | +| **Funcky.Xunit** | [![NuGet Version](https://img.shields.io/nuget/v/Funcky.Xunit)](https://www.nuget.org/packages/Funcky.Xunit) | `Assert.Some`, `Assert.Ok`, … for xUnit.net 2.x: unpack `Option`, `Result` and `Either` in tests. | | **Funcky.Xunit.v3** | [![NuGet Version](https://img.shields.io/nuget/v/Funcky.Xunit.v3)](https://www.nuget.org/packages/Funcky.Xunit.v3) | The same for xUnit.net 3.x, plus serializers to use the monads as theory data. | ### Interoperability @@ -112,7 +112,7 @@ Funcky adds the sequence operators LINQ lacks: windowing, pairing, interspersing `…OrNone` variants of every element query. They are lazy where LINQ is lazy and work together with the monads above. ```cs -Sequence.Return(1, 2, 3, 4) +Enumerable.Return(1, 2, 3, 4) .Pairwise((left, right) => left + right) // [3, 5, 7] .Intersperse(-1) // [3, -1, 5, -1, 7] .Inspect(Console.WriteLine) @@ -132,7 +132,7 @@ example from `option.Match(none: fallback, some: Identity)` to `option.GetOrElse * `Reader` and `Lazy` monads, and `Unit` as a first-class "no value" type. * Functional helpers: `Identity`, `Compose`, `Curry`, `Flip`, predicate composition, and `Retry` with configurable retry policies. -* «Constructors» for `IEnumerable`: `Sequence.Return`, `Sequence.Successors`, `Sequence.Cycle`, etc. +* «Constructors» for `IEnumerable`: `Enumerable.Return`, `Enumerable.Successors`, `Enumerable.Cycle`, etc. * `IAsyncEnumerable` counterparts of the sequence extensions, and `await` support for `Option>`. * Interoperability with `IQueryable`, `string`, `Stream`, `Nullable` and the numeric types. * Supports every framework from .NET Standard 2.0 and .NET Framework 4.8 to the newest .NET, and is trimming and diff --git a/changelog.md b/changelog.md index d68ecb003..d1ddf3a03 100644 --- a/changelog.md +++ b/changelog.md @@ -39,6 +39,20 @@ Funcky adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). to compile after the upgrade (the two delegates have different parameter types), swap its arguments. The `Match` rules of Funcky.Analyzers (λ1005 to λ1007 and λ1011) now tell the two arguments apart by parameter name, so they work with either order. +* Breaking: the static class `Funcky.Sequence` is removed. Its constructors live on as the static extension members of + `System.Linq.Enumerable` that 3.8.0 added (`Enumerable.Return`, `Enumerable.Successors`, `Enumerable.Cycle`, …), + declared in `Funcky.Extensions.EnumerableExtensions`. There is no 3.9 release to carry an obsoletion: on 3.8.0 set + the analyzer rule λ1202 to `warning`, apply its fix-all, then upgrade. λ1001 now proposes `Enumerable.Return` for + `Enumerable.Repeat(x, 1)` and λ1013 reports `Enumerable.Return(x)` where a collection expression fits. +* Breaking (Funcky.Xunit 3.0.0, Funcky.Xunit.v3 2.0.0): the static class `Funcky.FunctionalAssert` is removed. The + assertions are the static extension members of xunit's `Assert` that 3.8.0 added (`Assert.Some`, `Assert.None`, + `Assert.Ok`, `Assert.Error`, `Assert.Left`, `Assert.Right`), declared in `Funcky.Extensions.AssertExtensions`. On the + last 2.x release the analyzer rule λ1203 rewrites every call, as above. The failure message label changes with the + removal from `FunctionalAssert.Some() Failure` to `Assert.Some() Failure`, matching xunit's own `Assert.Equal() Failure`; + tests that assert on the message text need the new label. +* Breaking (Funcky.Analyzers 3.0.0): λ1202 and λ1203 retire, since the forms they pointed away from no longer exist; + their numbers stay reserved. λ1201 stays as an opt-in style rule, because both parse forms remain. The C# 14 language + gate of the `λ12xx` rules is gone: Funcky 4 requires the .NET 10 SDK, whose compiler always binds the extension forms. * Funcky and Funcky.Async add a `net11.0` asset and are built with the .NET 11 SDK (release candidate 3 while .NET 11 is in preview). The `net11.0` asset of Funcky.Async only forwards to Funcky, like the `net10.0` one. No target framework is removed in this step; see `SupportPolicy.md` for the frameworks Funcky 4 drops.