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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public sealed class ConsumeContextConverterGenerator : IIncrementalGenerator {
const string InterfaceName = "IMessageConsumeContext";
const string InterfaceFqn = $"{InterfaceNamespace}.{InterfaceName}`1";

// Keyword types must be emitted by their metadata names (System.String), because they cannot be qualified with global::
static readonly SymbolDisplayFormat TypeNameFormat = SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & ~SymbolDisplayMiscellaneousOptions.UseSpecialTypes
);

readonly struct KnownSymbols(INamedTypeSymbol? messageConsumeContext, INamedTypeSymbol? baseEventHandler) {
public INamedTypeSymbol? MessageConsumeContext { get; } = messageConsumeContext;
public INamedTypeSymbol? BaseEventHandler { get; } = baseEventHandler;
Expand Down Expand Up @@ -142,7 +147,9 @@ static bool IsPotentialUsage(SyntaxNode node, CancellationToken _) {
// so emitting switch arms by this count in descending order keeps the arm of a type ahead of the arms of its
// supertypes. Types related only through generic variance or array covariance get the same count.
static int GetSpecificity(ITypeSymbol symbol) {
var count = symbol.AllInterfaces.Length;
// An interface converts to object although object is not among its base types, so count it explicitly.
// It keeps object the only type with no supertypes, so its arm always goes last.
var count = symbol.AllInterfaces.Length + (symbol.TypeKind == TypeKind.Interface ? 1 : 0);

for (var baseType = symbol.BaseType; baseType != null; baseType = baseType.BaseType) count++;

Expand All @@ -153,12 +160,15 @@ static int GetSpecificity(ITypeSymbol symbol) {
// Skip unresolved generic type parameters (e.g. T in IMessageConsumeContext<T>)
if (symbol.TypeKind == TypeKind.TypeParameter) return null;

// Skip dynamic as it can't be used in a type pattern
if (symbol.TypeKind == TypeKind.Dynamic) return null;

// Skip types that are inaccessible from module-level generated code
// (e.g. private nested classes can't be referenced from the generated converter)
if (!IsAccessibleFromGeneratedCode(symbol)) return null;

// Use fully qualified name with global:: prefix
var name = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var name = symbol.ToDisplayString(TypeNameFormat);
return name.StartsWith("global::", StringComparison.Ordinal) ? name : $"global::{name}";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,76 @@ public static void Animals(IMessageConsumeContext<IEnvelope<Animal>> ctx) { }
await Assert.That(ArmIndex(generated, "global::Foo.IEnvelope<global::Foo.Zebra>")).IsLessThan(ArmIndex(generated, "global::Foo.IEnvelope<global::Foo.Animal>"));
}

[Test]
[Arguments("string", "global::System.String")]
[Arguments("object", "global::System.Object")]
[Arguments("string[]", "global::System.String[]")]
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
[Arguments("System.Collections.Generic.List<string>", "global::System.Collections.Generic.List<global::System.String>")]
public async Task Should_emit_compilable_arm_for_keyword_message_type(string messageType, string expectedArmType) {
// Issue #593: keyword types have no namespace to qualify, and 'global::string' is not valid C#
var source = $$"""
using Eventuous.Subscriptions.Context;

namespace Foo;

public static class Usages {
public static void Use(IMessageConsumeContext<{{messageType}}> ctx) { }
}
""";

var (generated, errors) = RunGenerator(source);

await Assert.That(errors).IsEmpty();
await Assert.That(ArmIndex(generated, expectedArmType)).IsGreaterThanOrEqualTo(0);
}

[Test]
public async Task Should_not_emit_arm_for_dynamic_message_type() {
// 'dynamic' can be neither qualified nor used in a type pattern
const string source = """
using Eventuous;
using Eventuous.Subscriptions.Context;

namespace Foo;

[EventType("V1.RoomBooked")]
public record RoomBooked;

public static class Usages {
public static void Use(IMessageConsumeContext<dynamic> ctx) { }
}
""";

var (generated, errors) = RunGenerator(source);

await Assert.That(errors).IsEmpty();
await Assert.That(generated).DoesNotContain("dynamic");
}

[Test]
public async Task Should_emit_object_after_interfaces() {
// Issue #594: an interface converts to object although object is not among its base types,
// and object is discovered first here
const string source = """
using Eventuous.Subscriptions.Context;

namespace Foo;

public interface IFoo;

public static class Usages {
public static void Any(IMessageConsumeContext<object> ctx) { }

public static void Foo(IMessageConsumeContext<IFoo> ctx) { }
}
""";

var (generated, errors) = RunGenerator(source);

await Assert.That(errors).IsEmpty();
await Assert.That(ArmIndex(generated, "global::Foo.IFoo")).IsLessThan(ArmIndex(generated, "global::System.Object"));
}

static int ArmIndex(string generated, string typeName) {
var index = generated.IndexOf($"{typeName} =>", StringComparison.Ordinal);

Expand Down
Loading