Repository navigation
Add analyzer λ1018: prefer Flatten over SelectMany with the identity function - #974
Merged
Merged
Conversation
…function SelectMany(x => x) and SelectMany(Identity) on a nested Option, Result, Either or IEnumerable collapse one level of nesting; Flatten() does the same and says so by name. The lambda may return its parameter through an implicit reference conversion (IEnumerable<int[]>), which Flatten accepts through the same covariance. The result-selector overload, query syntax, the static call form and expression trees are not reported. The code fix replaces the call and adds `using Funcky.Monads;` or `using Funcky.Extensions;` when the extension does not bind yet. The using helper takes the file's own line ending for a file without usings. Dogfooding: Sequence.Concat, CycleMaterialized, RepeatMaterialized and a test use Flatten; the four Flatten implementations suppress the rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unsafe replacements and syntax-handling failures can change behavior, break compilation, or prevent fixes from applying.
6 open issues
Preserve leading directives before inserting usings · New Avoid copying preprocessor directives into inserted usings · New Preserve explicit SelectMany result type in Flatten calls · New Track replacement nodes instead of reusing invalid spans · New Require successful binding to the intended Flatten extension · New Restrict SelectMany matching to supported methods · New
What changed in this PR
Adds λ1018 to recommend Flatten() for identity-based SelectMany calls, with a code fix, tests, documentation, and adoption in existing code.
Changes:
- Detects supported identity selectors and offers a
Flatten()replacement. - Adds namespace-import handling and analyzer tests.
- Documents the rule and updates existing flattening calls.
| File | Description |
|---|---|
| Funcky/Sequence/Sequence.RepeatMaterialized.cs | Uses Flatten(). |
| Funcky/Sequence/Sequence.CycleMaterialized.cs | Uses Flatten(). |
| Funcky/Sequence/Sequence.Concat.cs | Uses Flatten(). |
| Funcky/Monads/Result/ResultExtensions.cs | Suppresses λ1018 inside Flatten. |
| Funcky/Monads/Option/OptionExtensions.cs | Suppresses λ1018 inside Flatten. |
| Funcky/Monads/Either/EitherExtensions.cs | Suppresses λ1018 inside Flatten. |
| Funcky/Extensions/EnumerableExtensions/Flatten.cs | Suppresses λ1018 inside Flatten. |
| Funcky.Test/Extensions/EnumerableExtensions/SplitTest.cs | Uses Flatten() in a property test. |
| Funcky.Analyzers/Funcky.Analyzers/Flatten/PreferFlattenAnalyzer.cs | Implements λ1018 detection. |
| Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md | Registers the new rule. |
| Funcky.Analyzers/Funcky.Analyzers.Test/Flatten/PreferFlattenTest.cs | Tests diagnostics and fixes. |
| Funcky.Analyzers/Funcky.Analyzers.CodeFixes/Flatten/PreferFlattenCodeFix.cs | Implements the replacement fix. |
| Funcky.Analyzers/Funcky.Analyzers.CodeFixes/CodeAnalysisExtensions/UsingDirectives.cs | Adds namespace imports. |
| Documentation/src/SUMMARY.md | Links the rule documentation. |
| Documentation/src/analyzer-rules/λ1018.md | Explains the rule and examples. |
| Documentation/src/analyzer-rules/analyzer-rules.md | Adds λ1018 to the rule table. |
| changelog.md | Announces the analyzer and fix. |
Review effort: Balanced
…y, and behind leading #define directives The directive-safe insertion of the λ1017 fix moves into the shared UsingDirectives helper, which copies only whitespace and line endings from its neighbour. In a file without usings a leading `#define` or `#undef` moves in front of the new using, since those directives must precede every token. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and match only the owning SelectMany - The analyzer only reports the SelectMany of the monads and of System.Linq.Enumerable: ParallelEnumerable and Queryable have their own, which Flatten would turn into a plain enumerable. The rule uses the Funcky.Style category. - The fix keeps the last explicit type argument as `Flatten<T>()`, tracks the replacement with an annotation instead of the old span, and is only offered when the rewritten call (with the using added if necessary) binds to Funcky's Flatten; a Flatten member of the receiver's own type leaves the call reported without a fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
FreeApophis
enabled auto-merge
October 20, 2026 14:15
JudoJosef
approved these changes
Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
SelectMany(x => x)andSelectMany(Identity)on a nestedOption<Option<T>>,Result<Result<T>>,Either<L, Either<L, R>>orIEnumerable<IEnumerable<T>>isFlatten(). After λ1017 (Add analyzer λ1017: prefer the named function over the lambda #970) the lambda form becomesSelectMany(Identity), which makes the gap more visible, not less; this rule closes it.IEnumerable<int[]>.SelectMany(x => x)), sinceFlattenaccepts the receiver through the same covariance. Any type implementingIEnumerable<T>counts as receiver (List<int[]>).Enumerable.SelectMany(xs, x => x), expression trees, and compilations without the matchingFlattenextension.Flatten()and addsusing Funcky.Monads;/using Funcky.Extensions;when the extension does not bind yet (sameUsingDirectiveshelper as Add analyzer λ1012: prefer the …OrNone method over …OrDefault with a null check #973, here it also takes the file's own line ending for files without usings; I'll port that line to Add analyzer λ1012: prefer the …OrNone method over …OrDefault with a null check #973).Sequence.Concat,CycleMaterialized,RepeatMaterializedand one test now callFlatten(); the fourFlattenimplementations themselves suppress the rule with a pragma.λ1018.md, rule table, SUMMARY, changelog, AnalyzerReleases.Unshipped.Test plan
PreferFlattenTest: Option (lambda, block body, method group, chained), Result/Either, IEnumerable with using added andList<int[]>receiver, using added for a fully qualified monad type, the not-reported formsdotnet build Funcky.slnx -warnaserror --no-incrementalgreen; Sequence, Flatten and Split tests pass on net10.0🤖 Generated with Claude Code