diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 013eb12b..4938ae4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,9 @@ on: env: DOTNET_NOLOGO: 1 + # This is for some reason set to 'all' on CI which means + # we get warnings for transitive dependencies (e.g. from Messerli.CodeStyle -> StyleCop) + NuGetAuditMode: direct jobs: build: @@ -24,13 +27,9 @@ jobs: - uses: actions/setup-dotnet@v4 name: Install Current .NET SDK - uses: actions/setup-dotnet@v4 - name: 'Install .NET SDK 3.1' + name: 'Install .NET SDK 8.0' with: - dotnet-version: '3.1.x' - - uses: actions/setup-dotnet@v4 - name: 'Install .NET SDK 5.0' - with: - dotnet-version: '5.0.x' + dotnet-version: '8.0.x' - uses: actions/setup-dotnet@v4 name: 'Install .NET SDK 7.0' with: diff --git a/Directory.Build.props b/Directory.Build.props index 3b750436..1052c835 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,9 +23,6 @@ true true - - - $(MSBuildThisFileDirectory)artifacts diff --git a/Directory.Packages.props b/Directory.Packages.props index d25dc544..b7aded27 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,10 +12,9 @@ - + - diff --git a/FrameworkFeatureConstants.props b/FrameworkFeatureConstants.props index d37daa6d..b1e63c81 100644 --- a/FrameworkFeatureConstants.props +++ b/FrameworkFeatureConstants.props @@ -18,4 +18,7 @@ $(DefineConstants);RANDOM_SHUFFLE;UTF8_SPAN_PARSABLE + + $(DefineConstants);REFLECTION_ASSEMBLY_NAME_INFO;REFLECTION_TYPE_NAME + diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj b/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj index 3213f1ba..34e1bd7e 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 true true diff --git a/Funcky.Async.Test/Funcky.Async.Test.csproj b/Funcky.Async.Test/Funcky.Async.Test.csproj index fe3b8cd9..d956a47e 100644 --- a/Funcky.Async.Test/Funcky.Async.Test.csproj +++ b/Funcky.Async.Test/Funcky.Async.Test.csproj @@ -1,6 +1,6 @@ - net8.0;net7.0 + net9.0;net8.0;net7.0 preview enable false diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs index 479ab356..1460aaba 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs @@ -61,7 +61,7 @@ public static async IAsyncEnumerable Merge(this IEnumerable await HasMoreElements(f).ConfigureAwait(false)).ToListAsync().ConfigureAwait(false)), GetMergeComparer(comparer))) + await foreach (var element in MergeEnumerators(enumerators.RemoveRange(await enumerators.ToAsyncEnumerable().WhereAwait(async f => await HasMoreElements(f).ConfigureAwait(false)).ToListAsync().ConfigureAwait(false)), GetMergeComparer(comparer)).ConfigureAwait(false)) { yield return element; } diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/PowerSet.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/PowerSet.cs index a06f1970..66233fb4 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/PowerSet.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/PowerSet.cs @@ -18,21 +18,22 @@ public static IAsyncEnumerable> PowerSet(this IAsy private static async IAsyncEnumerable> PowerSetInternal(this IAsyncEnumerable source, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var asyncEnumerator = source.GetAsyncEnumerator(cancellationToken); - await using var sourceEnumerator = asyncEnumerator.ConfigureAwait(false); +#pragma warning disable CA2007 // Configured via IAsyncEnumerable extension + await using var asyncEnumerator = source.ConfigureAwait(false).WithCancellation(cancellationToken).GetAsyncEnumerator(); +#pragma warning restore CA2007 - await foreach (var set in PowerSetEnumerator(asyncEnumerator).WithCancellation(cancellationToken)) + await foreach (var set in PowerSetEnumerator(asyncEnumerator).WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return set; } } - private static async IAsyncEnumerable> PowerSetEnumerator(this IAsyncEnumerator source) + private static async IAsyncEnumerable> PowerSetEnumerator(this ConfiguredCancelableAsyncEnumerable.Enumerator source) { - if (await source.MoveNextAsync().ConfigureAwait(false)) + if (await source.MoveNextAsync()) { var temp = source.Current; - await foreach (var set in source.PowerSetEnumerator()) + await foreach (var set in source.PowerSetEnumerator().ConfigureAwait(false)) { yield return set; yield return set.Push(temp); diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/WithPrevious.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/WithPrevious.cs index 01ce13d8..b174482a 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/WithPrevious.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/WithPrevious.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace Funcky.Extensions; public static partial class AsyncEnumerableExtensions @@ -5,12 +7,16 @@ public static partial class AsyncEnumerableExtensions /// Returns a sequence mapping each element together with its predecessor. /// Thrown when any value in is . [Pure] - public static async IAsyncEnumerable> WithPrevious(this IAsyncEnumerable source) + public static IAsyncEnumerable> WithPrevious(this IAsyncEnumerable source) + where TSource : notnull + => source.WithPreviousInternal(); + + private static async IAsyncEnumerable> WithPreviousInternal(this IAsyncEnumerable source, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TSource : notnull { var previous = Option.None; - await foreach (var value in source) + await foreach (var value in source.ConfigureAwait(false).WithCancellation(cancellationToken)) { yield return new ValueWithPrevious(value, previous); previous = value; diff --git a/Funcky.Async/Funcky.Async.csproj b/Funcky.Async/Funcky.Async.csproj index a408cdfd..30b82f4a 100644 --- a/Funcky.Async/Funcky.Async.csproj +++ b/Funcky.Async/Funcky.Async.csproj @@ -1,6 +1,6 @@ - net8.0;net5.0;netstandard2.1;netstandard2.0 + net9.0;net8.0;net5.0;netstandard2.1;netstandard2.0 preview enable Extends Funcky with support for IAsyncEnumerable and Tasks. diff --git a/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj b/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj index 26d891a9..b50fdf5a 100644 --- a/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj +++ b/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj @@ -3,7 +3,7 @@ Funcky.SourceGenerator.Test Funcky.SourceGenerator.Test - net8.0 + net9.0 enable enable preview diff --git a/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.GenerateMethodWithDefaultValuedArgument.00.verified.cs b/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.GenerateMethodWithDefaultValuedArgument.00.verified.cs new file mode 100644 index 00000000..59c4638b --- /dev/null +++ b/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.GenerateMethodWithDefaultValuedArgument.00.verified.cs @@ -0,0 +1,12 @@ +//HintName: .g.cs +// +#nullable enable + +namespace Funcky.Extensions +{ + public static partial class ParseExtensions + { + [global::System.Diagnostics.Contracts.Pure] + public static Funcky.Monads.Option ParseTargetOrNone(this string candidate, string? hasDefault = null) => global::Funcky.Extensions.Target.TryParse(candidate, out var result, hasDefault) ? result : default(Funcky.Monads.Option); + } +} diff --git a/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.GenerateMethodWithDefaultValuedArgument.01.verified.cs b/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.GenerateMethodWithDefaultValuedArgument.01.verified.cs new file mode 100644 index 00000000..cbe9fb73 --- /dev/null +++ b/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.GenerateMethodWithDefaultValuedArgument.01.verified.cs @@ -0,0 +1,15 @@ +//HintName: OrNoneFromTryPatternAttribute.g.cs +namespace Funcky.Internal +{ + [global::System.Diagnostics.Conditional("COMPILE_TIME_ONLY")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true)] + internal class OrNoneFromTryPatternAttribute : global::System.Attribute + { + public OrNoneFromTryPatternAttribute(global::System.Type type, string method) + => (Type, Method) = (type, method); + + public global::System.Type Type { get; } + + public string Method { get; } + } +} diff --git a/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.cs b/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.cs index 364e2e6e..2a33b38a 100644 --- a/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.cs +++ b/Funcky.SourceGenerator.Test/OrNoneGeneratorSnapshotTests.cs @@ -46,6 +46,36 @@ public static bool TryParse(string candidate, out Target result) return TestHelper.Verify(source + Environment.NewLine + OptionSource); } + [Fact] + public Task GenerateMethodWithDefaultValuedArgument() + { + const string source = + """ + #nullable enable + + using Funcky.Internal; + + namespace Funcky.Extensions + { + [OrNoneFromTryPattern(typeof(Target), nameof(Target.TryParse))] + public static partial class ParseExtensions + { + } + + public sealed class Target + { + public static bool TryParse(string candidate, out Target result, string? hasDefault = null) + { + result = default!; + return false; + } + } + } + """; + + return TestHelper.Verify(source + Environment.NewLine + OptionSource); + } + [Fact] public Task GeneratesMethodWhenTargetIsNotNullableAnnotated() { diff --git a/Funcky.SourceGenerator/Extensions/IncrementalValuesProviderExtensions.cs b/Funcky.SourceGenerator/Extensions/IncrementalValuesProviderExtensions.cs deleted file mode 100644 index adac36d0..00000000 --- a/Funcky.SourceGenerator/Extensions/IncrementalValuesProviderExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Funcky.SourceGenerator.Extensions; - -internal static class IncrementalValuesProviderExtensions -{ - public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) - => source.Where(x => x is not null)!; -} diff --git a/Funcky.SourceGenerator/Funcky.SourceGenerator.csproj b/Funcky.SourceGenerator/Funcky.SourceGenerator.csproj index 8df35b48..b554c893 100644 --- a/Funcky.SourceGenerator/Funcky.SourceGenerator.csproj +++ b/Funcky.SourceGenerator/Funcky.SourceGenerator.csproj @@ -7,11 +7,11 @@ enable preview false + true - - - + + diff --git a/Funcky.SourceGenerator/OrNoneFromTryPatternGenerator.cs b/Funcky.SourceGenerator/OrNoneFromTryPatternGenerator.cs index b5155b8e..a5ab993a 100644 --- a/Funcky.SourceGenerator/OrNoneFromTryPatternGenerator.cs +++ b/Funcky.SourceGenerator/OrNoneFromTryPatternGenerator.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using Funcky.SourceGenerator.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -29,14 +28,13 @@ private static SourceProductionContext CreateSourceByClass(SourceProductionConte { var syntaxTree = OrNoneFromTryPatternPartial.GetSyntaxTree(methodByClass.First().NamespaceName, methodByClass.First().ClassName, methodByClass.SelectMany(m => m.Methods)); - context.AddSource($"{Path.GetFileName(methodByClass.Key)}.g.cs", string.Join(Environment.NewLine, GeneratedFileHeadersSource) + Environment.NewLine + syntaxTree.NormalizeWhitespace().ToFullString()); + context.AddSource($"{Path.GetFileName(methodByClass.Key)}.g.cs", string.Join("\n", GeneratedFileHeadersSource) + "\n" + syntaxTree.NormalizeWhitespace().ToFullString()); return context; } private static IncrementalValueProvider> GetOrNonePartialMethods(IncrementalGeneratorInitializationContext context) - => context.SyntaxProvider.CreateSyntaxProvider(predicate: IsSyntaxTargetForGeneration, transform: GetSemanticTargetForGeneration) - .WhereNotNull() + => context.SyntaxProvider.ForAttributeWithMetadataName(AttributeFullName, IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration) .Combine(context.CompilationProvider) .Select((state, _) => ToMethodPartial(state.Left, state.Right)) .Collect(); @@ -44,19 +42,11 @@ private static IncrementalValueProvider> GetOrNone private static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken cancellationToken) => node is ClassDeclarationSyntax { AttributeLists: [_, ..] }; - private static SemanticTarget? GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken cancellationToken) - => context.Node is ClassDeclarationSyntax classDeclarationSyntax - && context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken) is { } classSymbol - && classSymbol.GetAttributes() - .Where(a => a.AttributeClass?.ToDisplayString() == AttributeFullName) - .Where(AttributeBelongsToPartialPart(classDeclarationSyntax)) - .Select(ParseAttribute) - .ToImmutableArray() is [_, ..] attributes - ? new SemanticTarget(classDeclarationSyntax, attributes) - : null; - - private static Func AttributeBelongsToPartialPart(ClassDeclarationSyntax partialPart) - => attribute => attribute.ApplicationSyntaxReference?.GetSyntax().Ancestors().OfType().FirstOrDefault() == partialPart; + private static SemanticTarget GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + var node = (ClassDeclarationSyntax)context.TargetNode; + return new SemanticTarget(node, context.Attributes.Select(ParseAttribute).ToImmutableArray()); + } private static ParsedAttribute ParseAttribute(AttributeData attribute) => attribute.ConstructorArguments is [{ Value: INamedTypeSymbol type }, { Value: string methodName }, ..] @@ -177,9 +167,16 @@ private static string GetParameterName(ISymbol parameter, int index) private static EqualsValueClauseSyntax? GetParameterDefaultValue(IParameterSymbol parameter) => parameter.HasExplicitDefaultValue - ? throw new InvalidOperationException("Default values are not supported") + ? EqualsValueClause(GetLiteralForConstantValue(parameter.ExplicitDefaultValue, parameter.Type)) : null; + private static ExpressionSyntax GetLiteralForConstantValue(object? value, ITypeSymbol type) + => value switch + { + null => LiteralExpression(SyntaxKind.NullLiteralExpression), + _ => throw new NotSupportedException($"unsupported constant: {value} ({type})"), + }; + private static void RegisterOrNoneAttribute(IncrementalGeneratorPostInitializationContext context) => context.AddSource("OrNoneFromTryPatternAttribute.g.cs", CodeSnippets.OrNoneFromTryPatternAttribute); diff --git a/Funcky.Test/Extensions/DictionaryExtensionTest.cs b/Funcky.Test/Extensions/DictionaryExtensionTest.cs index ab5114c6..d75907f5 100644 --- a/Funcky.Test/Extensions/DictionaryExtensionTest.cs +++ b/Funcky.Test/Extensions/DictionaryExtensionTest.cs @@ -15,4 +15,11 @@ public void GivenADictionaryWhenWeLookForAnInexistentValueWithGetValueOrNoneThen var dictionary = new Dictionary { ["some"] = "value" }; FunctionalAssert.None(dictionary.GetValueOrNone(readOnlyKey: "none")); } + + [Fact] + public void CallingGetValueOrNoneOnADictionaryThatImplementsBothReadonlyAndNonReadonlyInterfacesIsNotACompileError() + { + var dictionary = new Dictionary { ["some"] = "value" }; + _ = dictionary.GetValueOrNone("some"); + } } diff --git a/Funcky.Test/Funcky.Test.csproj b/Funcky.Test/Funcky.Test.csproj index 9c8ba337..4a49af35 100644 --- a/Funcky.Test/Funcky.Test.csproj +++ b/Funcky.Test/Funcky.Test.csproj @@ -1,6 +1,6 @@ - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1 + net9.0;net8.0;net7.0;net6.0 $(TargetFrameworks);net4.8 preview enable diff --git a/Funcky.Xunit.Test/Funcky.Xunit.Test.csproj b/Funcky.Xunit.Test/Funcky.Xunit.Test.csproj index fca5771c..e23fb008 100644 --- a/Funcky.Xunit.Test/Funcky.Xunit.Test.csproj +++ b/Funcky.Xunit.Test/Funcky.Xunit.Test.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 preview enable false diff --git a/Funcky/CompatibilitySuppressions.xml b/Funcky/CompatibilitySuppressions.xml index 96b033ac..ee1f1edc 100644 --- a/Funcky/CompatibilitySuppressions.xml +++ b/Funcky/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0002 @@ -7,4 +7,16 @@ lib/net5.0/Funcky.dll lib/net6.0/Funcky.dll + + CP0021 + M:Funcky.Extensions.DictionaryExtensions.GetValueOrNone``2(System.Collections.Generic.IDictionary{``0,``1},``0)``0:notnull + lib/netstandard2.1/Funcky.dll + lib/netcoreapp3.1/Funcky.dll + + + CP0021 + M:Funcky.Extensions.DictionaryExtensions.GetValueOrNone``2(System.Collections.Generic.IReadOnlyDictionary{``0,``1},``0)``0:notnull + lib/netstandard2.1/Funcky.dll + lib/netcoreapp3.1/Funcky.dll + \ No newline at end of file diff --git a/Funcky/Extensions/DictionaryExtensions.cs b/Funcky/Extensions/DictionaryExtensions.cs index cab184d4..f942fecc 100644 --- a/Funcky/Extensions/DictionaryExtensions.cs +++ b/Funcky/Extensions/DictionaryExtensions.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace Funcky.Extensions; public static class DictionaryExtensions @@ -15,6 +17,7 @@ public static Option GetValueOrNone(this IDictionary.None; [Pure] + [OverloadResolutionPriority(1)] public static Option GetValueOrNone(this IReadOnlyDictionary dictionary, TKey readOnlyKey) #if NETCOREAPP3_1 // TKey was constraint to notnull when nullability annotations were originally added. It was later dropped again. diff --git a/Funcky/Extensions/ParseExtensions/ParseExtensions.AssemblyNameInfo.cs b/Funcky/Extensions/ParseExtensions/ParseExtensions.AssemblyNameInfo.cs new file mode 100644 index 00000000..0797d43b --- /dev/null +++ b/Funcky/Extensions/ParseExtensions/ParseExtensions.AssemblyNameInfo.cs @@ -0,0 +1,9 @@ +#if REFLECTION_ASSEMBLY_NAME_INFO +using System.Reflection.Metadata; +using Funcky.Internal; + +namespace Funcky.Extensions; + +[OrNoneFromTryPattern(typeof(AssemblyNameInfo), nameof(AssemblyNameInfo.TryParse))] +public static partial class ParseExtensions; +#endif diff --git a/Funcky/Extensions/ParseExtensions/ParseExtensions.TypeName.cs b/Funcky/Extensions/ParseExtensions/ParseExtensions.TypeName.cs new file mode 100644 index 00000000..f611e63f --- /dev/null +++ b/Funcky/Extensions/ParseExtensions/ParseExtensions.TypeName.cs @@ -0,0 +1,9 @@ +#if REFLECTION_TYPE_NAME +using System.Reflection.Metadata; +using Funcky.Internal; + +namespace Funcky.Extensions; + +[OrNoneFromTryPattern(typeof(TypeName), nameof(TypeName.TryParse))] +public static partial class ParseExtensions; +#endif diff --git a/Funcky/Funcky.csproj b/Funcky/Funcky.csproj index 1d7fcff8..e35de13e 100644 --- a/Funcky/Funcky.csproj +++ b/Funcky/Funcky.csproj @@ -1,6 +1,6 @@ - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1;netstandard2.0;netstandard2.1 + net9.0;net8.0;net7.0;net6.0;net5.0;netcoreapp3.1;netstandard2.0;netstandard2.1 preview enable Funcky @@ -18,7 +18,7 @@ $(DefineConstants);CONTRACTS_FULL - net8.0 + net9.0 diff --git a/Funcky/PublicAPI.Unshipped.txt b/Funcky/PublicAPI.Unshipped.txt index ad71e021..df864c0e 100644 --- a/Funcky/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI.Unshipped.txt @@ -3,3 +3,5 @@ Funcky.DownCast static Funcky.DownCast.From(Funcky.Monads.Option option) -> Funcky.Monads.Option static Funcky.DownCast.From(Funcky.Monads.Result result) -> Funcky.Monads.Result static Funcky.DownCast.From(Funcky.Monads.Either either, System.Func! failedCast) -> Funcky.Monads.Either +static Funcky.Extensions.ParseExtensions.ParseTypeNameOrNone(this System.ReadOnlySpan candidate, System.Reflection.Metadata.TypeNameParseOptions? options = null) -> Funcky.Monads.Option +static Funcky.Extensions.ParseExtensions.ParseAssemblyNameInfoOrNone(this System.ReadOnlySpan candidate) -> Funcky.Monads.Option diff --git a/global.json b/global.json index 3f809cbf..c6fbdb46 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "rollForward": "feature" } }