From 8aed59199780bfb89211b0a4e15585e2b0ac6d15 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Fri, 30 Oct 2026 08:43:07 +0100 Subject: [PATCH] Remove Option.FromBoolean and fix its calls on the compile error too Option.FromBoolean (all three overloads), obsolete since 3.8.0, is removed from Funcky 4.0.0 together with its tests; the PublicAPI files record the removal. The code fix in Funcky.Analyzers stays as the migration aid, but a fix on the obsoletion warning alone never fires once the method is gone, so it now also handles the compile error CS0117 a call of the removed member reports. Without a bound overload the fix reads the overload off the arguments: a lambda, a method group or a Func is the selector overload, everything else the value overload, and a selector whose return type cannot be told is left alone rather than risking the flattening Then applies. Closes #901 Co-Authored-By: Claude Fable 5.1 --- Documentation/src/option.md | 5 +- .../OptionFromBooleanCodeFix.cs | 267 +++++++++-- .../OptionFromBooleanRemovedCodeFixTest.cs | 440 ++++++++++++++++++ .../FunckyWellKnownTypeNames.cs | 2 + Funcky.Test/Monads/Option.FromBoolean.cs | 38 -- Funcky/Obsolete/Option/FromBoolean.cs | 43 -- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net11.0/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 3 + .../netstandard2.0/PublicAPI.Unshipped.txt | 3 + .../netstandard2.1/PublicAPI.Unshipped.txt | 3 + changelog.md | 5 + 13 files changed, 691 insertions(+), 127 deletions(-) create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/OptionFromBooleanRemovedCodeFixTest.cs delete mode 100644 Funcky.Test/Monads/Option.FromBoolean.cs delete mode 100644 Funcky/Obsolete/Option/FromBoolean.cs diff --git a/Documentation/src/option.md b/Documentation/src/option.md index 1faf915b0..71030a80a 100644 --- a/Documentation/src/option.md +++ b/Documentation/src/option.md @@ -62,8 +62,9 @@ Option name = user.HasName.Then(() => user.Name); // the select Option file = request.HasFormContentType.Then(() => request.Form.Files.FirstOrNone()); // flattening ``` -`Option.FromBoolean`, the previous form of this, is obsolete since 3.8.0 and will be removed in 4.0.0. The -`Funcky.Analyzers` package offers a code fix for the obsoletion warning that rewrites it to `Then`. +`Option.FromBoolean`, the previous form of this, was obsoleted in 3.8.0 and removed in 4.0.0. The `Funcky.Analyzers` +package offers a code fix that rewrites it to `Then`, both for the obsoletion warning on 3.8.0 and for the compile +error CS0117 the removed member leaves behind on 4.0.0, so the upgrade can go first and the rewrite second. The type parameter is constrained to `notnull`. An `Option` does not exist, because "absent" is what `None` is for. `Option.FromNullable` is the bridge: it maps `null` to `None` and anything else to `Some`. diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/OptionFromBooleanCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/OptionFromBooleanCodeFix.cs index a788508bc..da41ff796 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/OptionFromBooleanCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/OptionFromBooleanCodeFix.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Composition; +using System.Diagnostics.CodeAnalysis; using Funcky.Analyzers.CodeAnalysisExtensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; @@ -14,10 +15,13 @@ namespace Funcky.Analyzers; /// -/// Offers a fix for the obsoletion warning CS0618 on Option.FromBoolean, which bool.Then replaces: -/// Option.FromBoolean(condition, value) becomes condition.Then(value), the overload with a selector keeps -/// its selector, Option.FromBoolean(condition) becomes condition.Then(Unit.Value), and the flattening idiom -/// Option.FromBoolean(condition, option).SelectMany(Identity) becomes condition.Then(() => option). +/// Replaces a call of Option.FromBoolean with bool.Then: Option.FromBoolean(condition, value) becomes +/// condition.Then(value), the overload with a selector keeps its selector, Option.FromBoolean(condition) becomes +/// condition.Then(Unit.Value), and the flattening idiom Option.FromBoolean(condition, option).SelectMany(Identity) +/// becomes condition.Then(() => option). The fix is offered on the obsoletion warning CS0618 (Funcky 3.8.0) and on the +/// compile error CS0117 that a call of the removed method reports (Funcky 4.0.0), so a project can upgrade first and rewrite after. +/// With the member gone the compiler resolves no overload, so the fix reads it off the arguments: a lambda, a method group or a +/// Func<T> is the selector overload, everything else the value overload, as FromBoolean itself would have chosen. /// [Shared] [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionFromBooleanCodeFix))] @@ -26,7 +30,17 @@ public sealed class OptionFromBooleanCodeFix : ExtensionMemberCodeFix /// The compiler diagnostic reported on the use of a member marked [Obsolete] with a message. private const string ObsoleteMemberDiagnosticId = "CS0618"; - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(ObsoleteMemberDiagnosticId); + /// The compiler diagnostic reported on a member a type does not have, which a call of the removed method is. + private const string MissingMemberDiagnosticId = "CS0117"; + + /// The parameter names of the removed FromBoolean overloads, which named arguments still refer to. + private const string FromBooleanConditionParameterName = "boolean"; + + private const string FromBooleanItemParameterName = "item"; + + private const string FromBooleanSelectorParameterName = "selector"; + + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(ObsoleteMemberDiagnosticId, MissingMemberDiagnosticId); // The document is rewritten up front: the fix is withheld when the Then call would not bind to Funcky's extension. public override async Task RegisterCodeFixesAsync(CodeFixContext context) @@ -51,14 +65,10 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) protected override string GetTitle(ExtensionMemberRewrite rewrite) => $"Replace Option.{OptionFromBooleanMethodName} with {BooleanThenMethodName}"; - // The warning spans the whole call; one with a smaller span belongs to an obsolete member inside an argument. protected override ExtensionMemberRewrite? TryCreateRewrite(SyntaxNode root, SemanticModel semanticModel, Diagnostic diagnostic, CancellationToken cancellationToken) => GetSymbols(semanticModel.Compilation) is { } symbols - && root.FindInvocationExpression(diagnostic.Location.SourceSpan) is { } invocation - && invocation.Span == diagnostic.Location.SourceSpan - && semanticModel.GetOperation(invocation, cancellationToken) is IInvocationOperation { TargetMethod: { Name: OptionFromBooleanMethodName, IsStatic: true } method } operation - && SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.OptionType) - && Rewrite.TryCreate(operation, invocation, semanticModel, symbols, cancellationToken) is { } rewrite + && FindCall(root, semanticModel, diagnostic, symbols, cancellationToken) is { } call + && Rewrite.TryCreate(call, semanticModel, symbols, cancellationToken) is { } rewrite ? rewrite.ToExtensionMemberRewrite() : null; @@ -67,32 +77,206 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) ? BooleanThen.TryReplaceAllAsync(document, root, semanticModel, rewrites, booleanExtensionsType, cancellationToken) : Task.FromResult(null); + private static Call? FindCall(SyntaxNode root, SemanticModel semanticModel, Diagnostic diagnostic, Symbols symbols, CancellationToken cancellationToken) + => diagnostic.Id switch + { + ObsoleteMemberDiagnosticId => FindObsoleteCall(root, semanticModel, diagnostic, symbols, cancellationToken), + MissingMemberDiagnosticId => FindRemovedCall(root, semanticModel, diagnostic, symbols, cancellationToken), + _ => null, + }; + + // The warning spans the whole call; one with a smaller span belongs to an obsolete member inside an argument. + private static Call? FindObsoleteCall(SyntaxNode root, SemanticModel semanticModel, Diagnostic diagnostic, Symbols symbols, CancellationToken cancellationToken) + => root.FindInvocationExpression(diagnostic.Location.SourceSpan) is { } invocation + && invocation.Span == diagnostic.Location.SourceSpan + && semanticModel.GetOperation(invocation, cancellationToken) is IInvocationOperation { TargetMethod: { Name: OptionFromBooleanMethodName, IsStatic: true } method } operation + && SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.OptionType) + ? Call.FromOperation(operation, invocation) + : null; + + // The error spans the member name; the receiver in front of it still binds to the Option type once the member is gone. + private static Call? FindRemovedCall(SyntaxNode root, SemanticModel semanticModel, Diagnostic diagnostic, Symbols symbols, CancellationToken cancellationToken) + => root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is SimpleNameSyntax { Identifier.ValueText: OptionFromBooleanMethodName, Parent: MemberAccessExpressionSyntax { Expression: var receiver, Parent: InvocationExpressionSyntax invocation } memberAccess } name + && memberAccess.Name == name + && semanticModel.GetSymbolInfo(receiver, cancellationToken).Symbol is INamedTypeSymbol receiverType + && SymbolEqualityComparer.Default.Equals(receiverType, symbols.OptionType) + ? Call.FromSyntax(invocation, name, semanticModel, symbols, cancellationToken) + : null; + private static Symbols? GetSymbols(Compilation compilation) => compilation.GetOptionType() is { } optionType && compilation.GetOptionOfTType() is { } optionOfTType && compilation.GetUnitType() is { } unitType && compilation.GetBooleanExtensionsType() is { } booleanExtensionsType && BooleanThen.HasThen(booleanExtensionsType) - ? new Symbols(optionType, optionOfTType, unitType, booleanExtensionsType, compilation.GetFunctionalType()) + ? new Symbols(optionType, optionOfTType, unitType, booleanExtensionsType, compilation.GetFunctionalType(), compilation.GetFuncOfTType()) : null; - private sealed record Symbols(INamedTypeSymbol OptionType, INamedTypeSymbol OptionOfTType, INamedTypeSymbol UnitType, INamedTypeSymbol BooleanExtensionsType, INamedTypeSymbol? FunctionalType); + private static SimpleNameSyntax? GetMethodName(InvocationExpressionSyntax invocation) + => invocation.Expression switch + { + SimpleNameSyntax simpleName => simpleName, + MemberAccessExpressionSyntax memberAccess => memberAccess.Name, + _ => null, + }; + + private sealed record Symbols(INamedTypeSymbol OptionType, INamedTypeSymbol OptionOfTType, INamedTypeSymbol UnitType, INamedTypeSymbol BooleanExtensionsType, INamedTypeSymbol? FunctionalType, INamedTypeSymbol? FuncOfTType); + + /// An argument of the call in its position in the source, with the ordinal of the parameter it goes to. + private sealed record CallArgument(ArgumentSyntax Syntax, int Ordinal, IOperation? Value); + + /// A call of Option.FromBoolean as far as the rewrite needs to know it: bound by the compiler while the method is + /// still there, or reconstructed from the syntax and the types of the arguments once it is gone. is + /// the type argument of the called overload, for the overload without a value. + private sealed record Call(InvocationExpressionSyntax Invocation, SimpleNameSyntax Name, ImmutableArray Arguments, bool IsSelectorOverload, ITypeSymbol? ItemType, bool IsBound) + { + public static Call? FromOperation(IInvocationOperation operation, InvocationExpressionSyntax invocation) + { + if (GetMethodName(invocation) is not { } name) + { + return null; + } + + var arguments = operation.Arguments + .Select(argument => argument.Syntax is ArgumentSyntax syntax ? new CallArgument(syntax, argument.Parameter?.Ordinal ?? int.MaxValue, argument.Value) : null) + .ToImmutableArray(); + + if (arguments.Any(argument => argument is null)) + { + return null; + } + + // The definition tells the overloads apart; the constructed parameter type is a delegate for the value overload too when TItem is one. + var method = operation.TargetMethod; + var isSelectorOverload = method.OriginalDefinition.Parameters is [_, { Type.TypeKind: TypeKind.Delegate }]; + var itemType = method.TypeArguments is [var typeArgument] ? typeArgument : null; + + return new Call(invocation, name, arguments!, isSelectorOverload, itemType, IsBound: true); + } + + public static Call? FromSyntax(InvocationExpressionSyntax invocation, SimpleNameSyntax name, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) + { + var arguments = invocation.ArgumentList.Arguments + .Select((argument, index) => new CallArgument(argument, GetOrdinal(argument, index), semanticModel.GetOperation(argument.Expression, cancellationToken))) + .ToImmutableArray(); + + if (arguments.Any(argument => argument.Ordinal < 0) || arguments.Select(argument => argument.Ordinal).Distinct().Count() != arguments.Length) + { + return null; + } + + ITypeSymbol? explicitItemType = null; + + if (name is GenericNameSyntax genericName) + { + if (genericName.TypeArgumentList.Arguments is not [var typeArgument] || KnownType(semanticModel.GetTypeInfo(typeArgument, cancellationToken).Type) is not { } boundTypeArgument) + { + return null; + } + + explicitItemType = boundTypeArgument; + } + + return arguments switch + { + [{ Ordinal: 0 }] when explicitItemType is null => new Call(invocation, name, arguments, IsSelectorOverload: false, ItemType: null, IsBound: false), + [_, _] when InferOverload(arguments.First(argument => argument.Ordinal == 1), explicitItemType, semanticModel, symbols, cancellationToken) is var (isSelectorOverload, itemType) + => new Call(invocation, name, arguments, isSelectorOverload, itemType, IsBound: false), + _ => null, + }; + } + + private static int GetOrdinal(ArgumentSyntax argument, int position) + => argument.NameColon?.Name.Identifier.ValueText switch + { + null => position, + FromBooleanConditionParameterName => 0, + FromBooleanItemParameterName or FromBooleanSelectorParameterName => 1, + _ => -1, + }; + + /// Which overload the compiler would have picked for the value argument and with which type argument, or + /// when that cannot be told from the argument alone. + private static (bool IsSelectorOverload, ITypeSymbol ItemType)? InferOverload(CallArgument argument, ITypeSymbol? explicitItemType, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) + { + var isNamedItem = argument.Syntax.NameColon?.Name.Identifier.ValueText == FromBooleanItemParameterName; + var isNamedSelector = argument.Syntax.NameColon?.Name.Identifier.ValueText == FromBooleanSelectorParameterName; + var expression = argument.Syntax.Expression; + + if (explicitItemType is { TypeKind: TypeKind.Delegate } && !isNamedSelector) + { + return (false, explicitItemType); + } + + if (expression is AnonymousFunctionExpressionSyntax lambda) + { + return !isNamedItem && (explicitItemType ?? GetLambdaReturnType(lambda, semanticModel, cancellationToken)) is { } lambdaItemType + ? (true, lambdaItemType) + : null; + } + + if (KnownType(semanticModel.GetTypeInfo(expression, cancellationToken).Type) is { } type) + { + return !isNamedItem && IsFuncOfT(type, symbols, out var funcResultType) + ? (true, explicitItemType ?? funcResultType) + : !isNamedSelector + ? (false, explicitItemType ?? type) + : null; + } + + // A method group has no type of its own; it converts to Func with the return type of the method. + var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); + return !isNamedItem && (symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault()) is IMethodSymbol { ReturnsVoid: false } methodGroup + ? (true, explicitItemType ?? methodGroup.ReturnType) + : null; + } + + private static bool IsFuncOfT(ITypeSymbol type, Symbols symbols, [NotNullWhen(true)] out ITypeSymbol? resultType) + { + var isFuncOfT = type is INamedTypeSymbol { OriginalDefinition: var definition, TypeArguments: [_] } + && SymbolEqualityComparer.Default.Equals(definition, symbols.FuncOfTType); + resultType = isFuncOfT ? ((INamedTypeSymbol)type).TypeArguments[0] : null; + return isFuncOfT; + } + + // The compiler binds the body of a lambda even when the call around it does not; the type of what it returns is the item type. + private static ITypeSymbol? GetLambdaReturnType(AnonymousFunctionExpressionSyntax lambda, SemanticModel semanticModel, CancellationToken cancellationToken) + { + ExpressionSyntax?[] returnedExpressions = lambda.ExpressionBody is { } expressionBody + ? [expressionBody] + : lambda.Block?.DescendantNodes(node => node is not AnonymousFunctionExpressionSyntax and not LocalFunctionStatementSyntax) + .OfType() + .Select(returnStatement => returnStatement.Expression) + .ToArray() ?? []; + + var returnTypes = returnedExpressions + .Select(expression => expression is null ? null : KnownType(semanticModel.GetTypeInfo(expression, cancellationToken).Type)) + .ToArray(); + + return returnTypes.Length > 0 && returnTypes.All(type => type is not null) + ? returnTypes[0] + : null; + } + + private static ITypeSymbol? KnownType(ITypeSymbol? type) + => type is { TypeKind: not TypeKind.Error } ? type : null; + } /// What to replace (the FromBoolean call, or the SelectMany(Identity) around it) and with what. private sealed record Rewrite(InvocationExpressionSyntax Target, ExpressionSyntax Condition, SimpleNameSyntax ThenName, ArgumentSyntax ThenArgument) { - public static Rewrite? TryCreate(IInvocationOperation operation, InvocationExpressionSyntax invocation, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) + public static Rewrite? TryCreate(Call call, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) { - if (GetMethodName(invocation) is not { } name || !KeepsEvaluationOrder(operation)) + if (!KeepsEvaluationOrder(call.Arguments)) { return null; } // The arguments are in evaluation order, which named arguments can change. - return operation.Arguments.OrderBy(argument => argument.Parameter?.Ordinal ?? int.MaxValue).ToArray() switch + return call.Arguments.OrderBy(argument => argument.Ordinal).ToArray() switch { - [{ Syntax: ArgumentSyntax condition }] => new Rewrite(invocation, condition.Expression, BooleanThen.ThenName(name), Argument(UnitValue(symbols))), - [{ Syntax: ArgumentSyntax condition }, { Syntax: ArgumentSyntax argumentSyntax } argument] => TryCreate(operation, invocation, name, condition.Expression, argumentSyntax.Expression, argument.Value, semanticModel, symbols, cancellationToken), + [{ Syntax: var condition }] => new Rewrite(call.Invocation, condition.Expression, BooleanThen.ThenName(call.Name), Argument(UnitValue(symbols))), + [{ Syntax: var condition }, var argument] => TryCreate(call, condition.Expression, argument, semanticModel, symbols, cancellationToken), _ => null, }; } @@ -100,38 +284,36 @@ private sealed record Rewrite(InvocationExpressionSyntax Target, ExpressionSynta public ExtensionMemberRewrite ToExtensionMemberRewrite() => new(Target, BooleanThen.Invocation(Condition, ThenName, ThenArgument).WithTriviaFrom(Target)); - private static Rewrite? TryCreate(IInvocationOperation operation, InvocationExpressionSyntax invocation, SimpleNameSyntax name, ExpressionSyntax condition, ExpressionSyntax argument, IOperation value, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) + private static Rewrite? TryCreate(Call call, ExpressionSyntax condition, CallArgument argument, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) { - // The definition tells the overloads apart; the constructed parameter type is a delegate for the value overload too when TItem is one. - var isSelectorOverload = operation.TargetMethod.OriginalDefinition.Parameters[1].Type.TypeKind == TypeKind.Delegate; - var itemType = operation.TargetMethod.TypeArguments is [var typeArgument] ? typeArgument : null; - var producesNestedOption = itemType is INamedTypeSymbol { OriginalDefinition: var itemTypeDefinition } && SymbolEqualityComparer.Default.Equals(itemTypeDefinition, symbols.OptionOfTType); + var value = argument.Syntax.Expression; + var producesNestedOption = call.ItemType is INamedTypeSymbol { OriginalDefinition: var itemTypeDefinition } && SymbolEqualityComparer.Default.Equals(itemTypeDefinition, symbols.OptionOfTType); - return (FindSelectManyIdentity(invocation, semanticModel, symbols, cancellationToken), isSelectorOverload, producesNestedOption) switch + return (FindSelectManyIdentity(call, semanticModel, symbols, cancellationToken), call.IsSelectorOverload, producesNestedOption) switch { // Option.FromBoolean(condition, option).SelectMany(Identity) => condition.Then(() => option) - ({ } selectMany, false, true) => CanMoveIntoLambda(value) - ? new Rewrite(selectMany, condition, FlattenedThenName(name, (INamedTypeSymbol)itemType!), Argument(ParenthesizedLambdaExpression(argument.WithoutTrivia()))) + ({ } selectMany, false, true) => argument.Value is { } valueOperation && CanMoveIntoLambda(valueOperation) + ? new Rewrite(selectMany, condition, FlattenedThenName(call.Name, (INamedTypeSymbol)call.ItemType!), Argument(ParenthesizedLambdaExpression(value.WithoutTrivia()))) : null, // Option.FromBoolean(condition, () => option).SelectMany(Identity) => condition.Then(() => option) - ({ } selectMany, true, true) => new Rewrite(selectMany, condition, FlattenedThenName(name, (INamedTypeSymbol)itemType!), Argument(argument)), + ({ } selectMany, true, true) => new Rewrite(selectMany, condition, FlattenedThenName(call.Name, (INamedTypeSymbol)call.ItemType!), Argument(value)), // Option.FromBoolean(condition, () => option) yields an Option>, which Then would flatten. (_, true, true) => null, - (_, true, false) => new Rewrite(invocation, condition, BooleanThen.ThenName(name), Argument(argument)), + (_, true, false) => new Rewrite(call.Invocation, condition, BooleanThen.ThenName(call.Name), Argument(value)), - (_, false, _) => new Rewrite(invocation, condition, BooleanThen.ThenName(name), BooleanThen.ValueArgument(argument, itemType)), + (_, false, _) => new Rewrite(call.Invocation, condition, BooleanThen.ThenName(call.Name), BooleanThen.ValueArgument(value, call.ItemType)), }; } // Then evaluates the condition before the value. Named arguments given in the other order are only reordered when neither has an // effect the other could observe; the check is syntactic and errs on the side of keeping the call. - private static bool KeepsEvaluationOrder(IInvocationOperation operation) - => operation.Arguments.Select(argument => argument.Parameter?.Ordinal ?? int.MaxValue).ToArray() is var ordinals + private static bool KeepsEvaluationOrder(ImmutableArray arguments) + => arguments.Select(argument => argument.Ordinal).ToArray() is var ordinals && (ordinals.SequenceEqual(ordinals.OrderBy(ordinal => ordinal)) - || operation.Arguments.All(argument => argument.Syntax is ArgumentSyntax { Expression: var expression } && IsSideEffectFree(expression))); + || arguments.All(argument => IsSideEffectFree(argument.Syntax.Expression))); private static bool IsSideEffectFree(ExpressionSyntax expression) => expression switch @@ -164,27 +346,24 @@ private static SimpleNameSyntax FlattenedThenName(SimpleNameSyntax name, INamedT ? GenericName(Identifier(BooleanThenMethodName), TypeArgumentList(SingletonSeparatedList(ParseTypeName(nestedOptionType.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)).WithAdditionalAnnotations(Simplifier.Annotation)))) : IdentifierName(BooleanThenMethodName); - private static InvocationExpressionSyntax? FindSelectManyIdentity(InvocationExpressionSyntax invocation, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) - => invocation.Parent is MemberAccessExpressionSyntax { Name.Identifier.ValueText: SelectManyMethodName, Parent: InvocationExpressionSyntax { ArgumentList.Arguments: [{ Expression: var selector }] } selectMany } + // While the call is bound, the SelectMany has to be the one of Option; once the receiver is an error type the name has to do. + private static InvocationExpressionSyntax? FindSelectManyIdentity(Call call, SemanticModel semanticModel, Symbols symbols, CancellationToken cancellationToken) + => call.Invocation.Parent is MemberAccessExpressionSyntax { Name.Identifier.ValueText: SelectManyMethodName, Parent: InvocationExpressionSyntax { ArgumentList.Arguments: [{ Expression: var selector }] } selectMany } && symbols.FunctionalType is { } functionalType - && semanticModel.GetSymbolInfo(selectMany, cancellationToken).Symbol is IMethodSymbol { Name: SelectManyMethodName } selectManyMethod - && SymbolEqualityComparer.Default.Equals(selectManyMethod.ContainingType.OriginalDefinition, symbols.OptionOfTType) + && IsOptionSelectMany(semanticModel.GetSymbolInfo(selectMany, cancellationToken), call.IsBound, symbols) && IsIdentity(semanticModel.GetSymbolInfo(selector, cancellationToken), functionalType) ? selectMany : null; + private static bool IsOptionSelectMany(SymbolInfo symbolInfo, bool isBound, Symbols symbols) + => symbolInfo.Symbol is IMethodSymbol { Name: SelectManyMethodName } selectManyMethod + ? SymbolEqualityComparer.Default.Equals(selectManyMethod.ContainingType.OriginalDefinition, symbols.OptionOfTType) + : !isBound; + private static bool IsIdentity(SymbolInfo symbolInfo, INamedTypeSymbol functionalType) => (symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault()) is IMethodSymbol { Name: IdentityMethodName } identity && SymbolEqualityComparer.Default.Equals(identity.ContainingType, functionalType); - private static SimpleNameSyntax? GetMethodName(InvocationExpressionSyntax invocation) - => invocation.Expression switch - { - SimpleNameSyntax simpleName => simpleName, - MemberAccessExpressionSyntax memberAccess => memberAccess.Name, - _ => null, - }; - private static ExpressionSyntax UnitValue(Symbols symbols) => ParseExpression($"global::{symbols.UnitType.ToDisplayString()}.{UnitValuePropertyName}").WithAdditionalAnnotations(Simplifier.Annotation); } diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/OptionFromBooleanRemovedCodeFixTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/OptionFromBooleanRemovedCodeFixTest.cs new file mode 100644 index 000000000..7fce8f5f2 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/OptionFromBooleanRemovedCodeFixTest.cs @@ -0,0 +1,440 @@ +#pragma warning disable SA1118 // StyleCop support for collection expressions is missing +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Funcky.Analyzers.Test.CSharpCodeFixVerifier; + +namespace Funcky.Analyzers.Test; + +/// The fix on the compile error CS0117 that a call of Option.FromBoolean reports once the method is gone (Funcky 4.0.0). +/// The compiler resolves no overload there, so the fix has to tell the overloads apart from the arguments. +public sealed class OptionFromBooleanRemovedCodeFixTest +{ + private const string MissingMember = "CS0117"; + + private const string MissingXmlComment = "CS1591"; + + // language=csharp + private const string StubCode = + """ + namespace Funcky + { + public readonly struct Unit + { + public static Unit Value => default; + } + + public static class Functional + { + public static T Identity(T value) => value; + } + } + + namespace Funcky.Monads + { + public readonly struct Option + where TItem : notnull + { + public static Option None => default; + + public static implicit operator Option(TItem value) => default; + + public Option SelectMany(System.Func> selector) where TResult : notnull => default; + } + + public static class Option + { + public static Option Some(TItem value) where TItem : notnull => default; + } + } + + namespace Funcky.Extensions + { + using Funcky.Monads; + + public static class BooleanExtensions + { + public static Option Then(this bool condition, TItem value) where TItem : notnull => default; + + public static Option Then(this bool condition, System.Func selector) where TItem : notnull => default; + + public static Option Then(this bool condition, System.Func> selector) where TItem : notnull => default; + } + } + """; + + [Fact] + public async Task RewritesTheRemovedCallToThen() + { + const string inputCode = + """ + using Funcky; + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, int number, string text) + { + Use(Option.{|CS0117:FromBoolean|}(flag, number)); + Use(Option.{|CS0117:FromBoolean|}(number % 2 == 0, number)); + Use(Option.{|CS0117:FromBoolean|}(!flag, () => text)); + Use(Option.{|CS0117:FromBoolean|}(flag, text)); + Use(Option.{|CS0117:FromBoolean|}(flag)); + Use(Option.{|CS0117:FromBoolean|}(item: number, boolean: flag)); + Use(Option.{|CS0117:FromBoolean|}(flag, Next)); + Use(Option.{|CS0117:FromBoolean|}(flag, () => { return number; })); + Use(Funcky.Monads.Option.{|CS0117:FromBoolean|}(flag, number)); + } + + private static int Next() => 1; + + private static void Use(Option option) where T : notnull { } + } + """; + const string fixedCode = + """ + using Funcky; + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, int number, string text) + { + Use(flag.Then(number)); + Use((number % 2 == 0).Then(number)); + Use((!flag).Then(() => text)); + Use(flag.Then(text)); + Use(flag.Then(Unit.Value)); + Use(flag.Then(number)); + Use(flag.Then(Next)); + Use(flag.Then(() => { return number; })); + Use(flag.Then(number)); + } + + private static int Next() => 1; + + private static void Use(Option option) where T : notnull { } + } + """; + + await CreateTest(inputCode, fixedCode).RunAsync(CancellationToken.None); + } + + [Fact] + public async Task RewritesTheFlatteningIdiomToThenWithAnOptionSelector() + { + const string inputCode = + """ + using Funcky.Extensions; + using Funcky.Monads; + using static Funcky.Functional; + + public static class C + { + public static void M(bool flag, Option option) + { + Use(Option.{|CS0117:FromBoolean|}(flag, option).SelectMany(Identity)); + Use(Option.{|CS0117:FromBoolean|}(flag, () => option).SelectMany(Identity)); + Option a = Option.{|CS0117:FromBoolean>|}(flag, option).SelectMany(Identity); + Option> c = Option.{|CS0117:FromBoolean>|}(flag, option); + } + + private static void Use(Option option) where T : notnull { } + } + """; + const string fixedCode = + """ + using Funcky.Extensions; + using Funcky.Monads; + using static Funcky.Functional; + + public static class C + { + public static void M(bool flag, Option option) + { + Use(flag.Then(() => option)); + Use(flag.Then(() => option)); + Option a = flag.Then(() => option); + Option> c = flag.Then>(option); + } + + private static void Use(Option option) where T : notnull { } + } + """; + + await CreateTest(inputCode, fixedCode).RunAsync(CancellationToken.None); + } + + [Fact] + public async Task OffersNoFixForASelectorThatProducesANestedOption() + { + const string inputCode = + """ + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, Option option) + { + Use(Option.{|CS0117:FromBoolean|}(flag, () => option)); + Use(Option.{|CS0117:FromBoolean|}(flag, () => { return option; })); + Use(Option.{|CS0117:FromBoolean|}(flag, Fetch)); + } + + private static Option Fetch() => default; + + private static void Use(Option option) where T : notnull { } + } + """; + + await CreateTest(inputCode, inputCode).RunAsync(CancellationToken.None); + } + + [Fact] + public async Task AddsTheUsingForTheExtensionWhenMissing() + { + const string inputCode = + """ + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, int number) + { + Use(Option.{|CS0117:FromBoolean|}(flag, number)); + Use(Option.{|CS0117:FromBoolean|}(flag)); + Use(Option.{|CS0117:FromBoolean|}(flag, () => number)); + } + + private static void Use(Option option) where T : notnull { } + } + """; + const string fixedCode = + """ + using Funcky.Monads; + using Funcky.Extensions; + + public static class C + { + public static void M(bool flag, int number) + { + Use(flag.Then(number)); + Use(flag.Then(Funcky.Unit.Value)); + Use(flag.Then(() => number)); + } + + private static void Use(Option option) where T : notnull { } + } + """; + + await CreateTest(inputCode, fixedCode).RunAsync(CancellationToken.None); + } + + [Fact] + public async Task ReordersNamedArgumentsOnlyWhenTheyAreFreeOfSideEffects() + { + const string inputCode = + """ + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, int number, string text) + { + var n = 0; + Use(Option.{|CS0117:FromBoolean|}(item: number + 1, boolean: !flag)); + Use(Option.{|CS0117:FromBoolean|}(selector: () => text, boolean: flag)); + Use(Option.{|CS0117:FromBoolean|}(item: ++n, boolean: n == 1)); + Use(Option.{|CS0117:FromBoolean|}(item: Next(), boolean: flag)); + Use(Option.{|CS0117:FromBoolean|}(boolean: Next() == 1, item: Next())); + } + + private static int Next() => 1; + + private static void Use(Option option) where T : notnull { } + } + """; + const string fixedCode = + """ + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, int number, string text) + { + var n = 0; + Use((!flag).Then(number + 1)); + Use(flag.Then(() => text)); + Use(Option.FromBoolean(item: ++n, boolean: n == 1)); + Use(Option.FromBoolean(item: Next(), boolean: flag)); + Use((Next() == 1).Then(Next())); + } + + private static int Next() => 1; + + private static void Use(Option option) where T : notnull { } + } + """; + + var test = CreateTest(inputCode, fixedCode); + test.FixedState.ExpectedDiagnostics.AddRange( + [ + DiagnosticResult.CompilerError(MissingMember).WithSpan(11, 20, 11, 31), + DiagnosticResult.CompilerError(MissingMember).WithSpan(12, 20, 12, 31), + ]); + await test.RunAsync(CancellationToken.None); + } + + [Fact] + public async Task NamesTheValueParameterWhenTheValueIsADelegate() + { + const string inputCode = + """ + using System; + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, Func factory) + { + Option> a = Option.{|CS0117:FromBoolean|}(flag, item: factory); + Option> b = Option.{|CS0117:FromBoolean>|}(flag, factory); + Option> c = Option.{|CS0117:FromBoolean>|}(flag, () => 1); + Option d = Option.{|CS0117:FromBoolean|}(flag, factory); + } + } + """; + const string fixedCode = + """ + using System; + using Funcky.Extensions; + using Funcky.Monads; + + public static class C + { + public static void M(bool flag, Func factory) + { + Option> a = flag.Then(value: factory); + Option> b = flag.Then>(value: factory); + Option> c = flag.Then>(value: () => 1); + Option d = flag.Then(factory); + } + } + """; + + await CreateTest(inputCode, fixedCode).RunAsync(CancellationToken.None); + } + + [Fact] + public async Task OffersNoFlatteningFixWhenTheValueCannotMoveIntoALambda() + { + const string inputCode = + """ + using System.Threading.Tasks; + using Funcky.Extensions; + using Funcky.Monads; + using static Funcky.Functional; + + public struct S + { + private Option Field { get; } + + public async Task A(bool flag) + { + Use(Option.{|CS0117:FromBoolean|}(flag, await Fetch()).SelectMany(Identity)); + } + + public void M(bool flag, Option option, ref Option byRef) + { + Use(Option.{|CS0117:FromBoolean|}(flag, Field).SelectMany(Identity)); + Use(Option.{|CS0117:FromBoolean|}(flag, byRef).SelectMany(Identity)); + Use(Option.{|CS0117:FromBoolean|}(flag, option).SelectMany(Identity)); + } + + private static Task> Fetch() => Task.FromResult(Option.None); + + private static void Use(Option option) where T : notnull { } + } + """; + const string fixedCode = + """ + using System.Threading.Tasks; + using Funcky.Extensions; + using Funcky.Monads; + using static Funcky.Functional; + + public struct S + { + private Option Field { get; } + + public async Task A(bool flag) + { + Use(Option.FromBoolean(flag, await Fetch()).SelectMany(Identity)); + } + + public void M(bool flag, Option option, ref Option byRef) + { + Use(Option.FromBoolean(flag, Field).SelectMany(Identity)); + Use(Option.FromBoolean(flag, byRef).SelectMany(Identity)); + Use(flag.Then(() => option)); + } + + private static Task> Fetch() => Task.FromResult(Option.None); + + private static void Use(Option option) where T : notnull { } + } + """; + + var test = CreateTest(inputCode, fixedCode); + test.FixedState.ExpectedDiagnostics.AddRange( + [ + DiagnosticResult.CompilerError(MissingMember).WithSpan(12, 20, 12, 31), + DiagnosticResult.CompilerError(MissingMember).WithSpan(17, 20, 17, 31), + DiagnosticResult.CompilerError(MissingMember).WithSpan(18, 20, 18, 31), + ]); + await test.RunAsync(CancellationToken.None); + } + + [Fact] + public async Task OffersNoFixWhenThenWouldBindToAnotherExtension() + { + const string inputCode = + """ + using Funcky.Monads; + using Other; + + public static class C + { + public static void M(bool flag, int number) + { + Use(Option.{|CS0117:FromBoolean|}(flag, number)); + } + + private static void Use(Option option) where T : notnull { } + } + + namespace Other + { + public static class OtherExtensions + { + public static Option Then(this bool condition, T value) where T : notnull => default; + } + } + """; + + await CreateTest(inputCode, inputCode).RunAsync(CancellationToken.None); + } + + private static VerifyCS.Test CreateTest(string inputCode, string fixedCode) + => new() + { + TestCode = inputCode + Environment.NewLine + StubCode, + FixedCode = fixedCode + Environment.NewLine + StubCode, + DisabledDiagnostics = { MissingXmlComment }, + }; +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs index f23fd3939..14cc57950 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs @@ -34,6 +34,8 @@ public static class FunckyWellKnownTypeNames public static INamedTypeSymbol? GetExpressionOfTType(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Linq.Expressions.Expression`1"); + public static INamedTypeSymbol? GetFuncOfTType(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Func`1"); + public static INamedTypeSymbol? GetXunitAssertType(this Compilation compilation) => compilation.GetTypeByMetadataName("Xunit.Assert"); public static INamedTypeSymbol? GetFunctionalAssertType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.FunctionalAssert"); diff --git a/Funcky.Test/Monads/Option.FromBoolean.cs b/Funcky.Test/Monads/Option.FromBoolean.cs deleted file mode 100644 index 19dcd4b73..000000000 --- a/Funcky.Test/Monads/Option.FromBoolean.cs +++ /dev/null @@ -1,38 +0,0 @@ -#pragma warning disable CS0618 // FromBoolean is obsolete, it keeps working until 4.0.0 -using Funcky.Test.TestUtilities; - -namespace Funcky.Test.Monads; - -public sealed partial class OptionTest -{ - [Fact] - public void FromBooleanReturnsAMatchingOptionUnit() - { - Assert.None(Option.FromBoolean(false)); - Assert.Some(Unit.Value, Option.FromBoolean(true)); - } - - [Fact] - public void FromBooleanWithValueReturnsAMatchingOptionValue() - { - const int expectedValue = 1337; - - Assert.None(Option.FromBoolean(false, expectedValue)); - Assert.Some(expectedValue, Option.FromBoolean(true, expectedValue)); - } - - [Fact] - public void FromBooleanWithASelectorReturnsAMatchingOptionValue() - { - var expectedValue = 1337; - - Assert.None(Option.FromBoolean(false, () => expectedValue)); - Assert.Some(expectedValue, Option.FromBoolean(true, () => expectedValue)); - } - - [Fact] - public void TheBooleanSelectorIsLazyAndOnlyCalledWhenTheTrue() - { - Assert.None(Option.FromBoolean(false, FailOnCall.Function)); - } -} diff --git a/Funcky/Obsolete/Option/FromBoolean.cs b/Funcky/Obsolete/Option/FromBoolean.cs deleted file mode 100644 index ce6c5fa2d..000000000 --- a/Funcky/Obsolete/Option/FromBoolean.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.ComponentModel; -using static System.ComponentModel.EditorBrowsableState; - -namespace Funcky.Monads; - -public static partial class Option -{ - /// - /// Creates an from a boolean value. - /// - /// A boolean value. - /// boolean:true returns Some, boolean:false returns None. - [EditorBrowsable(Advanced)] - [Obsolete("Use condition.Then(Unit.Value) instead. This method will be removed in Funcky 4.0.0.")] - public static Option FromBoolean(bool boolean) - => boolean.Then(Unit.Value); - - /// - /// Returns an where the given item is returned when the boolean is true. - /// - /// Type of the returned item. - /// A boolean value. - /// A value which is returned when boolean is true. - /// boolean:true returns Some(item), boolean:false returns None. - [EditorBrowsable(Advanced)] - [Obsolete("Use condition.Then(item) instead. This method will be removed in Funcky 4.0.0.")] - public static Option FromBoolean(bool boolean, TItem item) - where TItem : notnull - => boolean.Then(item); - - /// - /// Returns an where the given item is returned from a selector function if the boolean is true. - /// - /// Type of the returned item. - /// A boolean value. - /// A function which returns a value of type TItem. - /// boolean:true returns Some(selector()), boolean:false returns None. - [EditorBrowsable(Advanced)] - [Obsolete("Use condition.Then(selector) instead. This method will be removed in Funcky 4.0.0.")] - public static Option FromBoolean(bool boolean, Func selector) - where TItem : notnull - => boolean.Then(selector); -} diff --git a/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 7dc5c5811..e72aadc33 100644 --- a/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option diff --git a/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt index ce46d7500..ba81baeda 100644 --- a/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net11.0/PublicAPI.Unshipped.txt @@ -45,3 +45,6 @@ static Funcky.Extensions.StringExtensions.LastIndexOfOrNone(this string! haystac static Funcky.Extensions.StringExtensions.LastIndexOfOrNone(this string! haystack, System.Text.Rune value, int startIndex, int count, System.StringComparison comparisonType) -> Funcky.Monads.Option static Funcky.Extensions.StringExtensions.LastIndexOfOrNone(this string! haystack, System.Text.Rune value, int startIndex, System.StringComparison comparisonType) -> Funcky.Monads.Option static Funcky.Extensions.StringExtensions.LastIndexOfOrNone(this string! haystack, System.Text.Rune value, System.StringComparison comparisonType) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option diff --git a/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 7dc5c5811..e72aadc33 100644 --- a/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option diff --git a/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 7dc5c5811..e72aadc33 100644 --- a/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option diff --git a/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c5811..e72aadc33 100644 --- a/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option diff --git a/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 7dc5c5811..e72aadc33 100644 --- a/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, System.Func! selector) -> Funcky.Monads.Option +*REMOVED*static Funcky.Monads.Option.FromBoolean(bool boolean, TItem item) -> Funcky.Monads.Option diff --git a/changelog.md b/changelog.md index 10d4e5934..aff1791f1 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,11 @@ Funcky adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `netstandard2.1` asset (or `netstandard2.0` below .NET Core 3.0), which lacks the members that need a newer base class library, such as the generic math and `IParsable` based parsers, `DateOnly`/`TimeOnly` and the trimming annotations. +* Breaking: `Option.FromBoolean` (all three overloads), obsolete since 3.8.0, is removed. Use `Then` on the condition: + `condition.Then(value)`, `condition.Then(() => value)` and `condition.Then(Unit.Value)` for the overload without a + value. The code fix in Funcky.Analyzers 3.0.0 now also fires on the compile error CS0117 that the removed member + leaves behind, so a project can upgrade to Funcky 4.0.0 first and let the fix (or Fix-All) rewrite every call + afterwards; the fix for the obsoletion warning CS0618 on Funcky 3.8.0 stays as well. * 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.