Skip to content

fix(subscriptions): generate valid converter arms for keyword message types - #595

Merged
alexeyzimarev merged 2 commits into
devfrom
fix/593-594-converter-generator-keyword-types
Sep 21, 2026
Merged

alexeyzimarev merged 2 commits into
devfrom
fix/593-594-converter-generator-keyword-types

Conversation

@alexeyzimarev

@alexeyzimarev alexeyzimarev commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #593
Fixes #594

Two related problems in ConsumeContextConverterGenerator, fixed together because the second only becomes reachable once the first is fixed.

Keyword message types (#593)

GetTypeSyntax formatted type names with SymbolDisplayFormat.FullyQualifiedFormat and prepended global:: when the name did not already start with it. That format includes UseSpecialTypes, so keyword types come back as string / object, and the generator emitted global::string, which is not valid C#. A keyword message type, or an array of one, broke the consuming build with CS1041.

Names are now formatted without UseSpecialTypes, so these types are emitted by their metadata names:

Message type Before After
string global::string (CS1041) global::System.String
object global::object (CS1041) global::System.Object
string[] global::string[] (CS1525 and others) global::System.String[]

Keywords nested in generic arguments were already fine and still are; they now render as List<global::System.String> instead of List<string>.

dynamic is in the same family but cannot be fixed by renaming: it can be neither qualified nor used in a type pattern, and it produced global::dynamic (CS0400). It is now skipped, so such messages go through the existing fallback conversion. This was not in the issue; I found it while testing the keyword cases.

object arm ordered before an interface arm (#594)

With object compiling, its arm could be emitted ahead of an interface arm and subsume it (CS8510). The specificity rank from #592 counts base classes plus all interfaces, and an interface converts to object although object is not among its base types, so object and an interface with no base interfaces both had rank 0 and kept discovery order.

The rank now counts object for interfaces. The strict ordering still holds (a class implementing I counts object, I and everything I counts), and object is left as the only type with rank 0, so its arm always goes last.

Tests

Added to ConsumeContextConverterGeneratorTests, each seen failing first:

  • Should_emit_compilable_arm_for_keyword_message_type for string, object and string[]
  • Should_not_emit_arm_for_dynamic_message_type
  • Should_emit_object_after_interfaces, with object discovered before the interface

Added after review: a List<string> case in the keyword test, pinning the nested rendering List<global::System.String>. It passed straight away since the fix was already in place, so I checked it by restoring the old name format, which makes it fail.

Verified locally on net10.0 only (143 tests in the project pass, generator builds with no warnings, and the Mongo projection tests and the Postgres Bookings sample build against the patched generator); net8.0 and net9.0 runtimes are not installed on my machine, so those rely on CI.

No public API change, so no docs or plugin updates are needed.

🤖 Generated with Claude Code

… types (#593, #594)

ConsumeContextConverterGenerator formatted type names with
FullyQualifiedFormat, which renders keyword types as `string` / `object`
without a global:: prefix, and then prepended global:: itself. The
resulting `global::string` is not valid C#, so a keyword message type, or
an array of one, broke the build with CS1041. Format names without
UseSpecialTypes so these types are emitted by their metadata names
(global::System.String).

Skip `dynamic` altogether: it can be neither qualified nor used in a type
pattern, so it produced `global::dynamic` (CS0400).

With `object` now compiling, its arm could be ordered ahead of an
interface arm and subsume it (CS8510): an interface converts to object,
but object is not among its base types, so both had specificity zero.
Count object for interfaces, which leaves object as the only type with
no supertypes, so its arm always goes last.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-21T08:13:14.595036Z a9f5620 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Generate valid converter arms for keyword message types

🐞 Bug fix 🧪 Tests 🕐 10-20 Minutes

Grey Divider

AI Description

• Emit keyword message types with fully qualified metadata names in generated converter arms.
• Skip dynamic candidates and rank interfaces above object to prevent invalid or unreachable
 patterns.
• Cover keyword, dynamic, and interface/object ordering regressions with generator compilation
 tests.
Diagram

graph TD
    A["Context usages"] --> B["Type candidates"] --> C["Candidate filtering"] --> D["Metadata names"] --> E["Specificity ranking"] --> F["Sorted switch arms"] --> G["Generated converter"]
Loading
High-Level Assessment

The current approach is appropriate: customizing Roslyn's existing fully qualified display format fixes all special-type spellings without maintaining a manual keyword map, while explicitly excluding dynamic reflects its type-pattern limitation. Adjusting the existing specificity metric is also less invasive than replacing the stable ordering pipeline; rebuilding generation around SyntaxFactory would add complexity without improving this focused fix.

Files changed (2) +81 / -2

Bug fix (1) +12 / -2
ConsumeContextConverterGenerator.csGenerate valid and correctly ordered converter type patterns +12/-2

Generate valid and correctly ordered converter type patterns

• Formats special C# types using fully qualified metadata names, preventing invalid forms such as global::string. Dynamic candidates are excluded, and interfaces receive an additional specificity rank so their switch arms precede System.Object.

src/Core/gen/Eventuous.Subscriptions.Generators/ConsumeContextConverterGenerator.cs

Tests (1) +69 / -0
ConsumeContextConverterGeneratorTests.csAdd converter generator regression coverage for keyword types +69/-0

Add converter generator regression coverage for keyword types

• Adds compilation tests for string, object, and string-array converter arms, verifies dynamic produces no arm, and confirms interface patterns are emitted before object even when object is discovered first.

src/Core/test/Eventuous.Tests.Subscriptions/ConsumeContextConverterGeneratorTests.cs

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Generic keyword regressions go untested ✓ Resolved 📎 Requirement gap ⚙ Maintainability
Description
The parameter set for Should_emit_compilable_arm_for_keyword_message_type covers string,
object, and string[] but omits the required List case. A later change can corrupt nested
keyword formatting while all three existing checks remain green, leaving
global::System.Collections.Generic.List unenforced.
Code

src/Core/test/Eventuous.Tests.Subscriptions/ConsumeContextConverterGeneratorTests.cs[R90-92]

+    [Arguments("string", "global::System.String")]
+    [Arguments("object", "global::System.Object")]
+    [Arguments("string[]", "global::System.String[]")]
Evidence
Compliance rule 1 explicitly requires generator coverage for List and its metadata-style fully
qualified output. The added parameterized test lists only string, object, and string[], so
that required case is absent.

Emit valid fully qualified names for keyword message types
src/Core/test/Eventuous.Tests.Subscriptions/ConsumeContextConverterGeneratorTests.cs[89-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The keyword-type regression test omits the explicitly required `List<string>` case, so nested keyword formatting is not verified.
## Fix Focus Areas
- src/Core/test/Eventuous.Tests.Subscriptions/ConsumeContextConverterGeneratorTests.cs[89-108]
## Recommended Fix
Add a test argument using `System.Collections.Generic.List<string>` as the message type and assert that its arm is emitted as `global::System.Collections.Generic.List<global::System.String>`, while retaining the compilation-error assertion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Test Results

   48 files  ± 0     48 suites  ±0   15m 9s ⏱️ + 1m 16s
  613 tests + 6    613 ✅ + 6  0 💤 ±0  0 ❌ ±0 
1 226 runs  +18  1 226 ✅ +18  0 💤 ±0  0 ❌ ±0 

Results for commit 9d35447. ± Comparison against base commit 9179885.

This pull request removes 9 and adds 15 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(09/20/2026 13:56:11 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(09/20/2026 13:56:11)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(e84a1182-897c-4bdc-80d7-198ffaa192ea)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:13.8414022+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-09-20T13:51:13.8414022+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:13.8414022+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:19.7740242+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-09-20T13:51:19.7740242+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:19.7740242+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:20.5923844+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-09-20T13:51:20.5923844+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-20T13:51:20.5923844+00:00 })
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(09/21/2026 10:35:09 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(09/21/2026 10:35:09)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(d3278afd-6ebc-49e0-9754-ef2ca02914d6)
Eventuous.Tests.Subscriptions.ConsumeContextConverterGeneratorTests ‑ Should_emit_compilable_arm_for_keyword_message_type(System·Collections·Generic·List<string>, global::System·Collections·Generic·List<global::System·String>)
Eventuous.Tests.Subscriptions.ConsumeContextConverterGeneratorTests ‑ Should_emit_compilable_arm_for_keyword_message_type(object, global::System·Object)
Eventuous.Tests.Subscriptions.ConsumeContextConverterGeneratorTests ‑ Should_emit_compilable_arm_for_keyword_message_type(string, global::System·String)
Eventuous.Tests.Subscriptions.ConsumeContextConverterGeneratorTests ‑ Should_emit_compilable_arm_for_keyword_message_type(string[], global::System·String[])
Eventuous.Tests.Subscriptions.ConsumeContextConverterGeneratorTests ‑ Should_emit_object_after_interfaces
Eventuous.Tests.Subscriptions.ConsumeContextConverterGeneratorTests ‑ Should_not_emit_arm_for_dynamic_message_type
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-09-21T10:29:43.0021447+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T10:29:43.0021447+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-09-21T10:29:43.0021447+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-09-21T10:29:43.0021447+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-09-21T10:29:43.0021447+00:00 })
…

♻️ This comment has been updated with latest results.

Formatting type names without UseSpecialTypes also changes how a keyword
nested in generic arguments is rendered: List<string> is now emitted as
List<global::System.String>. Pin that in the keyword message type test;
the case fails when the old name format is restored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit ed6cfd1 into dev Sep 21, 2026
17 checks passed
@alexeyzimarev
alexeyzimarev deleted the fix/593-594-converter-generator-keyword-types branch September 21, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant