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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Documentation/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
13 changes: 6 additions & 7 deletions Documentation/src/analyzer-rules/analyzer-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -51,16 +52,14 @@ 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 |
| [λ1017](./λ1017.md) | Warning | Prefer the named function over the lambda |
| [λ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

Expand Down
8 changes: 4 additions & 4 deletions Documentation/src/analyzer-rules/λ1001.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -21,7 +21,7 @@ IEnumerable<string> greeting = Enumerable.Repeat("hello", 1);

### Allowed
```cs
IEnumerable<string> greeting = Sequence.Return("hello");
IEnumerable<string> greeting = Enumerable.Return("hello");

// Repeating a value more than once is fine.
IEnumerable<string> echo = Enumerable.Repeat("hello", 3);
Expand Down
20 changes: 10 additions & 10 deletions Documentation/src/analyzer-rules/λ1013.md
Original file line number Diff line number Diff line change
@@ -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<T>`,
`IReadOnlyCollection<T>` or `IReadOnlyList<T>`.

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
Expand All @@ -32,10 +32,10 @@ without a left operand is parenthesized so that it does not turn into a spread.
### Disallowed
```cs
static IEnumerable<int> Example()
=> Sequence.Return(42);
=> Enumerable.Return(42);

static void Example()
=> Consume(Sequence.Return("tag"));
=> Consume(Enumerable.Return("tag"));

static void Consume(IReadOnlyList<string> tags) { }
```
Expand All @@ -50,9 +50,9 @@ static void Example()

// No target type: the receiver of a LINQ chain.
static IEnumerable<int> 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<object> Example()
=> Sequence.Return("tag");
=> Enumerable.Return("tag");
```
17 changes: 9 additions & 8 deletions Documentation/src/analyzer-rules/λ1101.md
Original file line number Diff line number Diff line change
@@ -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>(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>(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.

Expand All @@ -23,7 +24,7 @@ Use the overload that accepts the expected value as its first argument. A code f
public void ReturnsTheAnswer()
{
Option<int> option = Option.Some(42);
Assert.Equal(42, FunctionalAssert.Some(option));
Assert.Equal(42, Assert.Some(option));
}
```

Expand All @@ -33,15 +34,15 @@ public void ReturnsTheAnswer()
public void ReturnsTheAnswer()
{
Option<int> option = Option.Some(42);
FunctionalAssert.Some(42, option);
Assert.Some(42, option);
}

// Unwrapping and then asserting something other than equality is fine.
[Fact]
public void ReturnsAPositiveNumber()
{
Option<int> option = Option.Some(42);
var value = FunctionalAssert.Some(option);
var value = Assert.Some(option);
Assert.True(value > 0);
}
```
7 changes: 4 additions & 3 deletions Documentation/src/analyzer-rules/λ1201.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>` or a `ReadOnlySpan<byte>`,
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<TEnum>`) and the generic forms (`ParseOrNone<T>`, `ParseNumberOrNone<T>`); the parsed type comes
from the `Option<T>` the extension returns.
Expand All @@ -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
Expand Down
38 changes: 0 additions & 38 deletions Documentation/src/analyzer-rules/λ1202.md

This file was deleted.

30 changes: 0 additions & 30 deletions Documentation/src/analyzer-rules/λ1203.md

This file was deleted.

2 changes: 1 addition & 1 deletion Documentation/src/case-studies/calendar-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 9 additions & 9 deletions Documentation/src/case-studies/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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));
```

Expand All @@ -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)
```
Expand All @@ -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);
```
Expand All @@ -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)
Expand Down Expand Up @@ -152,7 +152,7 @@ private static IEnumerable<string> LayoutMonth(IEnumerable<DateOnly> 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<T>`, 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<string> LayoutMonth(IEnumerable<DateOnly> month)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -316,7 +316,7 @@ Funcky has a lazy `Transpose` extension function for `IEnumerable<IEnumerable<T>

```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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Documentation/src/case-studies/case-studies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Documentation/src/case-studies/csv-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ static string Report(IReadOnlyList<Transaction> 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
Expand Down
Loading
Loading