Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8dbbfa
Make Ix homoiconicity generator nullable-aware
idg10 Jun 28, 2024
3467b0b
Upgrade ApiCompare to .NET 8.0 and fix diagnostics
idg10 Jun 28, 2024
4033ce5
Update Playground to .NET 8.0 and fix diagnostics
idg10 Jun 28, 2024
0b7d822
Fix System.Interactive diagnostics on .NET SDK 8.0
idg10 Jun 28, 2024
74349ca
Update System.Interactive.Async for .NET 8.0 SDK
idg10 Jun 28, 2024
83b1c8a
Update System.Linq.Async for .NET SDK 8.0
idg10 Jun 28, 2024
da36955
Update System.Linq.Async.Queryable for .NET SDK 8.0
idg10 Jun 28, 2024
b237b92
Update System.Linq.Async.Tests for .NET SDK 8.0
idg10 Jul 1, 2024
6cced7c
Update System.Interactive.Async.Tests for .NET SDK 8.0
idg10 Jul 1, 2024
fd11594
Update System.Interactive.Tests for .NET SDK 8.0
idg10 Jul 1, 2024
98f1bce
More minor 8.0 SDK fixes for System.Linq.Async.Tests
idg10 Jul 1, 2024
9f83243
Correct the version #if for a couple of warnings
idg10 Jul 1, 2024
eb582d6
Stop diagnostics for all possible primary ctor usage
idg10 Jul 1, 2024
376f633
Add readme explaining use of .NET Core 3.1 in tests
idg10 Jul 1, 2024
2e23112
Remove MSBuild.Sdk.Extras
idg10 Jul 2, 2024
480d064
Fix some more straggling compiler diagnostics
idg10 Jul 2, 2024
f90cf1b
Add step to build refs explicitly
idg10 Jul 2, 2024
fbf08f3
Fix conditional package ref in System.Linq.Async ref assembly project
idg10 Jul 2, 2024
51799f6
Add missing config to new build pipeline step
idg10 Jul 3, 2024
0735f4a
Fix typos in Ix ref assembly ADR
idg10 Jul 3, 2024
1e7dac3
Specify build config via arguments
idg10 Jul 3, 2024
4da89e1
Add API approvals to ensure we don't accidentally change the API
idg10 Jul 3, 2024
8758930
Add reference to .NET Core 3.1 explainer readme in test projects
idg10 Jul 4, 2024
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
34 changes: 34 additions & 0 deletions Ix.NET/Source/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[*.cs]

# Prevent IDE1006 (Naming rule violation) errors for non-public fields.
#
# Unfortunately, the codebase has not historically been entirely consistent with internal naming
# conventions. Apparently this wasn't noticed by older analyzer tooling, but as of the .NET 7
# era, the naming rules analyzers are a bit more particular, and cannot be configured in a way
# that makes them happy with the code as it stands. We could rename all the relevant symbols,
# but that doesn't seem like an especially good use of time, so for now, we're suppressing
# diagnostics in certain cases.
#
# Static readonly fields
#dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.severity = none

# Internal fields
dotnet_naming_symbols.internal_field_symbols.applicable_kinds = field
dotnet_naming_symbols.internal_field_symbols.applicable_accessibilities = internal

dotnet_naming_rule.internal_instance_fields_must_be_camel_cased_underscore_prefix.symbols = internal_field_symbols
dotnet_naming_rule.internal_instance_fields_must_be_camel_cased_underscore_prefix.style = camel_case_and_prefix_with_underscore_style
#dotnet_naming_rule.internal_instance_fields_must_be_camel_cased_underscore_prefix.severity = none


# Protected fields
# Annoyingly, a protected field in an internal class cannot be distinguished from a protected field in a public
# class. That's unfortunate, because the latter are publicly visible, but the former are not, so we don't really
# want to enforce public naming conventions on them. We generally avoid publicly visible fields, so the majority
# of protected fields are in fact in internal types, so we use naming conventions appropriate to those.
dotnet_naming_symbols.protected_field_symbols.applicable_kinds = field
dotnet_naming_symbols.protected_field_symbols.applicable_accessibilities = protected

dotnet_naming_rule.protected_instance_fields_must_be_camel_cased_underscore_prefix.symbols = protected_field_symbols
dotnet_naming_rule.protected_instance_fields_must_be_camel_cased_underscore_prefix.style = camel_case_and_prefix_with_underscore_style
dotnet_naming_rule.protected_instance_fields_must_be_camel_cased_underscore_prefix.severity = none
2 changes: 1 addition & 1 deletion Ix.NET/Source/ApiCompare/ApiCompare.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
204 changes: 91 additions & 113 deletions Ix.NET/Source/ApiCompare/Program.cs

Large diffs are not rendered by default.

79 changes: 61 additions & 18 deletions Ix.NET/Source/AsyncQueryableGenerator.t4
Original file line number Diff line number Diff line change
@@ -1,43 +1,84 @@
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Runtime" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Threading" #>
<#@ import namespace="System.Threading.Tasks" #>
<#@ import namespace="System.Collections.Generic" #>
<#
var infoFieldNames = new Dictionary<string, int>();

var toQuotedImpl = default(Func<Type, int, bool, string>);
toQuotedImpl = (t, i, b) =>
var toQuotedImpl = default(Func<Type, ParameterInfo, int, bool, string>);
toQuotedImpl = (t, paramObjectForAttributes, parameterIndex, notNestedGenericTypeParameter) =>
{
var name = t.Name;

// We always want to look at the whole-type nullability, so we look at that now, and if it's a generic
// type, we'll also go on to inspect per-type-parameter nullability.
var nullableData = paramObjectForAttributes?.GetCustomAttributesData().SingleOrDefault(ad => ad.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
bool wholeTypeNullable = false;
if (nullableData is not null)
{
if (nullableData.ConstructorArguments[0].Value is IReadOnlyList<CustomAttributeTypedArgument> nullableFlags)
{
// When we end up in this part, the type argument is, in practice, being used as the type argument to
// the actual type. E.g., ValueTask<TSource?>. So we need to look at the second nullable flag.
wholeTypeNullable = ((byte)nullableFlags[1].Value) == 2;
}
else if (nullableData.ConstructorArguments[0].Value is byte bv)
{
wholeTypeNullable = bv == 2;
}
}
if (t.IsGenericType)
{
var genDef = t.GetGenericTypeDefinition();
name = genDef.Name.Substring(0, genDef.Name.LastIndexOf('`'));

var genArgs = "<" + string.Join(", ", t.GetGenericArguments().Select(a => toQuotedImpl(a, i, false))) + ">";
Type[] genericArguments = t.GetGenericArguments();
bool[] typeParamsNullable = new bool[genericArguments.Length];
if (nullableData is not null)
{
if (nullableData.ConstructorArguments[0].Value is IReadOnlyList<CustomAttributeTypedArgument> nullableFlags)
{
// There isn't a 1-1 correspondence between type parameters and nullable flags. Really we should be recursively
// walking the type parameters, but this hack suffices for the types we actually encounter in practice.
int flagIndex = 1;
for (int i = 0; i < typeParamsNullable.Length; ++i)
{
if (!genericArguments[i].IsValueType)
{
if (flagIndex >= nullableFlags.Count) throw new InvalidOperationException($"Type {t} has {typeParamsNullable.Length} type params, but the associated nullable attribute on parameterinto {paramObjectForAttributes} has length {nullableFlags.Count}");
typeParamsNullable[i] = ((byte)nullableFlags[flagIndex].Value) == 2;
flagIndex += 1;
}
}
}
}

var genArgs = "<" + string.Join(", ", genericArguments.Select((a, i) => toQuotedImpl(a, null, parameterIndex, false) + (typeParamsNullable[i] ? "?" : ""))) + ">";

if (b)
if (notNestedGenericTypeParameter)
{
if (name == "Func" || name == "Action")
{
name = "Expression<" + name + genArgs + ">";
}
else if (name == "IAsyncEnumerable" && i == 0)
else if (name == "IAsyncEnumerable" && parameterIndex == 0)
{
name = "IAsyncQueryable" + genArgs;
}
else if (name == "IOrderedAsyncEnumerable" && i == 0)
else if (name == "IOrderedAsyncEnumerable" && parameterIndex == 0)
{
name = "IOrderedAsyncQueryable" + genArgs;
}
else
{
name += genArgs;
}

//if (wholeTypeNullable) { name += "?"; }
}
else
{
Expand All @@ -48,12 +89,13 @@ if (t.IsGenericType)
else
{
name += genArgs;
if (wholeTypeNullable) { name += "?"; }
}
}
}
else if (t.IsArray)
{
var elem = toQuotedImpl(t.GetElementType(), i, b);
var elem = toQuotedImpl(t.GetElementType(), null, parameterIndex, notNestedGenericTypeParameter);
name = elem + "[]";
}
else
Expand Down Expand Up @@ -86,12 +128,13 @@ else
{
name = "object";
}
if (wholeTypeNullable) { name += "?"; }
}

return name;
};

var toQuoted = new Func<Type, int, string>((t, i) => toQuotedImpl(t, i, true));
var toQuoted = new Func<Type, ParameterInfo, int, string>((t, paramObjectForAttributes, parameterIndex) => toQuotedImpl(t, paramObjectForAttributes, parameterIndex, true));
#>
#nullable enable

Expand Down Expand Up @@ -125,16 +168,16 @@ foreach (var m in asyncEnumerableType.GetMethods()
.OrderBy(m => m.Name)
.ThenBy(m => m.IsGenericMethod ? m.GetGenericArguments().Length : 0)
.ThenBy(m => m.GetParameters().Length)
.ThenBy(m => string.Join(", ", m.GetParameters().Select((p, i) => toQuoted(p.ParameterType, i) + " " + p.Name))))
.ThenBy(m => string.Join(", ", m.GetParameters().Select((p, i) => toQuoted(p.ParameterType, p, i) + " " + p.Name))))
{
var genArgs = m.GetGenericArguments();

var ret = toQuoted(m.ReturnType, 0);
var ret = toQuoted(m.ReturnType, m.ReturnParameter, 0);
var name = m.Name;

if (genArgs.Length > 0)
{
name += "<" + string.Join(", ", genArgs.Select(a => a.Name)) + ">";
name += "<" + string.Join(", ", genArgs.Select((a, i) => a.Name)) + ">";
}

var isParams = false;
Expand All @@ -156,8 +199,8 @@ foreach (var m in asyncEnumerableType.GetMethods()
}
}

var pars = string.Join(", ", m.GetParameters().Select((p, i) => (i == parCount - 1 && isParams ? "params " : "") + toQuoted(p.ParameterType, i) + (nullableParameterNames.Contains(p.Name) ? "?" : "") + " " + p.Name + (i == parCount - 1 && lastParameterDefault ? " = default" : "")));
var quotedPars = string.Join(", ", m.GetParameters().Select((p, i) => "default(" + toQuoted(p.ParameterType, i) + ")"));
var pars = string.Join(", ", m.GetParameters().Select((p, i) => (i == parCount - 1 && isParams ? "params " : "") + toQuoted(p.ParameterType, p, i) + (nullableParameterNames.Contains(p.Name) ? "?" : "") + " " + p.Name + (i == parCount - 1 && lastParameterDefault ? " = default" : "")));
var quotedPars = string.Join(", ", m.GetParameters().Select((p, i) => "default(" + toQuoted(p.ParameterType, p, i) + ")"));

if (m.IsDefined(typeof(System.Runtime.CompilerServices.ExtensionAttribute), true))
{
Expand Down Expand Up @@ -193,7 +236,7 @@ foreach (var m in asyncEnumerableType.GetMethods()

infoName += infoNameId;

var infoSignature = string.Join(", ", m.GetParameters().Select((p, i) => toQuoted(p.ParameterType, i)).Concat(new[] { toQuoted(m.ReturnType, 0) }));
var infoSignature = string.Join(", ", m.GetParameters().Select((p, i) => toQuoted(p.ParameterType, p, i)).Concat(new[] { toQuoted(m.ReturnType, m.ReturnParameter, 0) }));

foreach (var genArg in genArgs)
{
Expand All @@ -219,7 +262,7 @@ foreach (var m in asyncEnumerableType.GetMethods()

if (td.Name.EndsWith("Task`1")) // NB: Covers Task and ValueTask
{
factory = "ExecuteAsync<" + toQuotedImpl(m.ReturnType.GetGenericArguments()[0], -1, false) + ">";
factory = "ExecuteAsync<" + toQuotedImpl(m.ReturnType.GetGenericArguments()[0], m.ReturnParameter, -1, false) + ">";

var last = m.GetParameters().Last();
if (last.ParameterType == typeof(CancellationToken))
Expand All @@ -233,11 +276,11 @@ foreach (var m in asyncEnumerableType.GetMethods()
}
else if (td == typeof(IAsyncEnumerable<>) || td == typeof(IOrderedAsyncEnumerable<>))
{
factory = "CreateQuery<" + toQuotedImpl(m.ReturnType.GetGenericArguments()[0], -1, false) + ">";
factory = "CreateQuery<" + toQuotedImpl(m.ReturnType.GetGenericArguments()[0], null, -1, false) + ">";

if (td == typeof(IOrderedAsyncEnumerable<>))
{
cast = "(" + toQuoted(m.ReturnType, 0) + ")";
cast = "(" + toQuoted(m.ReturnType, null, 0) + ")";
}
}
}
Expand Down Expand Up @@ -274,7 +317,7 @@ foreach (var m in asyncEnumerableType.GetMethods()

if (!add)
{
quotedArgs.Add("Expression.Constant(" + p.Name + ", typeof(" + toQuoted(pt, -1) + "))");
quotedArgs.Add("Expression.Constant(" + p.Name + ", typeof(" + toQuoted(pt, null, -1) + "))");
}

n++;
Expand Down
6 changes: 5 additions & 1 deletion Ix.NET/Source/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)ReactiveX.snk</AssemblyOriginatorKeyFile>
<NoWarn>$(NoWarn);1701;1702;CS1591;NU5105</NoWarn>
<!--
IDE0056 Suggests using range-based indexing, which doesn't work on .NET 4.8 (even in cases where it would compile down to code with no dependency on Index or Range).
IDE0270 Null check can be simplified. Tends to suggest using for exception throwing cases even when the resulting code would be overly complex.
-->
<NoWarn>$(NoWarn);1701;1702;CS1591;NU5105;IDE0056;IDE0270</NoWarn>
<DefaultLanguage>en-US</DefaultLanguage>
<IsTestProject>$(MSBuildProjectName.Contains('Test'))</IsTestProject>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
Expand Down
12 changes: 3 additions & 9 deletions Ix.NET/Source/Playground/DemoAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@
namespace Playground
{
[AttributeUsage(AttributeTargets.Method)]
internal sealed class DemoAttribute : Attribute
internal sealed class DemoAttribute(int index, string title) : Attribute
{
public DemoAttribute(int index, string title)
{
Index = index;
Title = title;
}

public int Index { get; }
public string Title { get; }
public int Index { get; } = index;
public string Title { get; } = title;
}
}
2 changes: 1 addition & 1 deletion Ix.NET/Source/Playground/Playground.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
28 changes: 15 additions & 13 deletions Ix.NET/Source/Playground/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information.

#pragma warning disable IDE0051 // Remove unused private members - all used via reflection

using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -10,22 +12,22 @@

namespace Playground
{
class Program
internal class Program
{
static void Main()
private static void Main()
{
RunDemos();
}

[Demo(0, "Random experimentation")]
static async Task Experiment()
private static async Task Experiment()
{
// Add test code here
await Task.Yield(); // Suppress CS1998
}

[Demo(11, "LINQ to Objects for IEnumerable<T>")]
static void Linq()
private static void Linq()
{
var xs = new List<int> { 1, 2, 3 };
var ys = xs.Where(x => x % 2 == 0);
Expand All @@ -37,7 +39,7 @@ static void Linq()
}

[Demo(12, "LINQ to Objects for IQueryable<T>")]
static void LinqQueryable()
private static void LinqQueryable()
{
var xs = new List<int> { 1, 2, 3 }.AsQueryable();
var ys = xs.Where(x => x % 2 == 0);
Expand All @@ -49,7 +51,7 @@ static void LinqQueryable()
}

[Demo(21, "LINQ to Objects for IEnumerable<T> - Interactive Extensions")]
static void Ix()
private static void Ix()
{
var xs = new List<int> { 1, 2, 3 };
var ys = xs.Distinct(x => x % 2);
Expand All @@ -61,7 +63,7 @@ static void Ix()
}

[Demo(22, "LINQ to Objects for IQueryable<T> - Interactive Extensions")]
static void IxQueryable()
private static void IxQueryable()
{
var xs = new List<int> { 1, 2, 3 }.AsQueryable();
var ys = xs.Distinct(x => x % 2);
Expand All @@ -73,7 +75,7 @@ static void IxQueryable()
}

[Demo(31, "LINQ to Objects for IAsyncEnumerable<T>")]
static async Task AsyncLinq()
private static async Task AsyncLinq()
{
var xs = new List<int> { 1, 2, 3 };
var ys = xs.ToAsyncEnumerable().Where(x => x % 2 == 0);
Expand Down Expand Up @@ -103,7 +105,7 @@ static async Task AsyncLinq()
}

[Demo(32, "LINQ to Objects for IAsyncQueryable<T>")]
static async Task AsyncLinqQueryable()
private static async Task AsyncLinqQueryable()
{
var xs = new List<int> { 1, 2, 3 }.AsQueryable();
var ys = xs.ToAsyncEnumerable().Where(x => x % 2 == 0);
Expand Down Expand Up @@ -133,7 +135,7 @@ static async Task AsyncLinqQueryable()
}

[Demo(41, "LINQ to Objects for IAsyncEnumerable<T> - Interactive Extensions")]
static async Task AsyncIx()
private static async Task AsyncIx()
{
var xs = new List<int> { 1, 2, 3 };
var ys = xs.ToAsyncEnumerable().Distinct(x => x % 2);
Expand All @@ -152,7 +154,7 @@ await ys.ForEachAsync(y =>
}

[Demo(42, "LINQ to Objects for IAsyncQueryable<T> - Interactive Extensions")]
static async Task AsyncIxQueryable()
private static async Task AsyncIxQueryable()
{
var xs = new List<int> { 1, 2, 3 }.AsQueryable();
var ys = xs.ToAsyncEnumerable().Distinct(x => x % 2);
Expand All @@ -170,7 +172,7 @@ await ys.ForEachAsync(y =>
#endif
}

static void RunDemos()
private static void RunDemos()
{
var methods = (from method in typeof(Program).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
let demo = method.GetCustomAttribute<DemoAttribute>()
Expand All @@ -197,7 +199,7 @@ orderby demo.Index
while (retry)
{
Console.Write("Enter demo [C: Clear, X: Exit]: ");
var input = Console.ReadLine().Trim().ToUpper();
var input = Console.ReadLine()?.Trim().ToUpper();

switch (input)
{
Expand Down
Loading