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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.6.1] - 2026-04-30

### Fixed

- **Aliased `encodingType` on `<enum>` and `<set>` (#164)**: Per SBE 1.0 spec the `encodingType` attribute is a `symbolicName_t` and may reference either a primitive type name or a user-declared `<type>` whose `primitiveType` supplies the underlying wire type (a common FIX pattern, e.g. B3 EntryPoint's `uint8EnumEncoding`). The generator now resolves such aliases to the underlying primitive instead of emitting the literal alias name (which produced `CS0246`/`CS1008`). Advisory `presence`, `semanticType`, and `nullValue` on the alias remain ignored for the enum's underlying type, as the spec intends.
- **Pipeline robustness on duplicate generated source names (#164)**: A duplicate `hintName` from `AddSource` previously threw `ArgumentException` and aborted the entire generator phase, cascading into thousands of `CS0246` errors against partially-emitted files. The generator now suppresses duplicates with the new diagnostic `SBE015` (Warning) and continues emitting the rest of the schema. Per-item failures are also caught so one bad item does not derail the rest of its phase.

### Added

- **`SBE015` diagnostic** — *Duplicate generated source suppressed*: emitted when the generator produces two source files with the same `hintName`. The first occurrence is kept; the duplicate is dropped. Resolve the underlying schema duplication (e.g., two `<enum>` declarations sharing a name) or the upstream generator path that produced the second source.
- **Regression coverage for B3-style schemas**: new unit tests for aliased `encodingType` resolution (uint8/uint16/char aliases, fixed-size char arrays excluded from alias registration), driver-level tests for `SBE015`, and an integration schema (`bcl-collision-schema.xml`) exercising an SBE enum named `Boolean` to validate generated code compiles cleanly under `ImplicitUsings=enable` and `TreatWarningsAsErrors=true`.
- **End-to-end coverage of the B3 Binary EntryPoint v8.4.2 vendor schema**: the full schema is now included as an integration fixture (`tests/SbeCodeGenerator.IntegrationTests/TestSchemas/b3-entrypoint-messages-8.4.2.xml`, sourced from B3's public distribution) with a smoke test (`B3EntryPointTests`) that exercises the aliased-`encodingType` fix, the BCL `Boolean` collision workaround, and a full encode/decode round-trip on the `Sequence` message.

### Notes

- Issue #164 originally also reported `CS0117 'bool' does not contain a definition for 'TRUE_VALUE'` on schemas declaring `<enum name="Boolean">`. With the cascade in (#164/2) fixed, generated code itself compiles cleanly because C# name resolution prefers types in the enclosing namespace over those imported via `using` directives. Consumer code that imports the schema's namespace alongside `System` may still see `CS0104` (ambiguous reference) and should disambiguate via a `using` alias (e.g. `using Boolean = MySchema.Boolean;`) or fully qualified names — this is C# language semantics, not a generator bug.

## [1.6.0] - 2026-04-29

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A Roslyn-based source generator that converts FIX Simple Binary Encoding (SBE) X
- Explicit `blockLength` on messages
- Validation constraints (min/max ranges)
- Zero-cost `SbeDispatcher` + `ISbeMessageHandler` for devirtualized message routing
- Comprehensive build-time diagnostics (SBE001–SBE014)
- Comprehensive build-time diagnostics (SBE001–SBE015)

## What's New in v1.5.0

Expand Down Expand Up @@ -369,6 +369,7 @@ The generator provides comprehensive diagnostics:
| SBE012 | Warning | Invalid SbeAssumeHostEndianness value |
| SBE013 | Warning | Duplicate type name |
| SBE014 | Warning | sinceVersion exceeds schema version |
| SBE015 | Warning | Duplicate generated source hintName suppressed |

See [Diagnostics README](./src/SbeCodeGenerator/Diagnostics/README.md) for details.

Expand Down
10 changes: 10 additions & 0 deletions src/SbeCodeGenerator/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## Release 1.6.1

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|------
SBE013 | SbeSourceGenerator | Warning | Duplicate type name in schema.
SBE014 | SbeSourceGenerator | Warning | sinceVersion exceeds schema version.
SBE015 | SbeSourceGenerator | Warning | Duplicate generated source hintName suppressed.

## Release 0.8.0

### New Rules
Expand Down
2 changes: 0 additions & 2 deletions src/SbeCodeGenerator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|------
SBE013 | SbeSourceGenerator | Warning | Duplicate type name in schema.
SBE014 | SbeSourceGenerator | Warning | sinceVersion exceeds schema version.
18 changes: 18 additions & 0 deletions src/SbeCodeGenerator/Diagnostics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ Provides compile-time diagnostics for:
**Example**: `<SbeAssumeHostEndianness>Invalid</SbeAssumeHostEndianness>`
**Resolution**: Set the value to `LittleEndian` or `BigEndian`, or remove the property entirely for automatic detection.

### SBE013: Duplicate Type Name
**Severity**: Warning
**Triggered when**: Two `<type>`, `<enum>`, `<set>`, or `<composite>` declarations share the same name within a single schema
**Example**: Two `<enum name="Side">` blocks in the same `<types>` section
**Resolution**: Rename one of the duplicates so each generated identifier is unique. The generator uses the last definition encountered.

### SBE014: sinceVersion Exceeds Schema Version
**Severity**: Warning
**Triggered when**: A field's `sinceVersion` attribute is greater than the schema's `version` attribute
**Example**: `<field sinceVersion="3"/>` in a schema declared as `version="1"`
**Resolution**: Either bump the schema version, or correct the field's `sinceVersion`. Such fields are unreachable in any generated message version.

### SBE015: Duplicate Generated Source Suppressed
**Severity**: Warning
**Triggered when**: The generator produces two source files with the same `hintName` (Roslyn requires uniqueness). The first is kept; the duplicate is suppressed and generation continues.
**Example**: A schema declares two `<enum name="Side">` blocks; the second pass attempts `AddSource("…/Enums/Side.cs", …)` again. Without the suppression, Roslyn would throw `ArgumentException`, abort the generator phase, and produce a cascade of `CS0246` errors against partially-emitted files.
**Resolution**: Resolve the underlying duplication in the schema (commonly a duplicate type name — see also `SBE013`) or fix the upstream code path that emitted the second source.

## Usage

Diagnostics are automatically reported during source generation. When you build a project that includes an invalid SBE schema as an additional file, you'll see these diagnostics in:
Expand Down
10 changes: 10 additions & 0 deletions src/SbeCodeGenerator/Diagnostics/SbeDiagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,15 @@ internal static class SbeDiagnostics
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "A field's sinceVersion attribute should not exceed the schema's version attribute. Such fields are unreachable and may indicate a schema authoring error.");

// SBE015: Duplicate generated source hintName suppressed
public static readonly DiagnosticDescriptor DuplicateGeneratedSource = new DiagnosticDescriptor(
id: "SBE015",
title: "Duplicate generated source suppressed",
messageFormat: "Duplicate generated source hintName '{0}' produced by phase '{1}' was suppressed; the first occurrence is kept. This usually indicates a duplicate type definition in the schema or an upstream generator bug.",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Roslyn requires every generated source hintName to be unique. The generator now suppresses duplicates and continues, instead of aborting the entire generation phase. Resolve the underlying schema duplication or fix the upstream generator path that produced the second source.");
}
}
24 changes: 24 additions & 0 deletions src/SbeCodeGenerator/Generators/TypeResolverHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ internal static class TypeResolverHelper

public static bool IsDotNetPrimitive(string typeName) => DotNetPrimitiveTypes.Contains(typeName);

/// <summary>
/// Resolves an <c>encodingType</c> attribute (from &lt;enum&gt; or &lt;set&gt;) to the
/// underlying SBE primitive type name (e.g., "uint8", "char").
/// Per SBE 1.0 spec, <c>encodingType</c> is a <c>symbolicName_t</c> and may
/// reference either a primitive type name or a user-declared &lt;type&gt; alias
/// such as <c>&lt;type name="uint8EnumEncoding" primitiveType="uint8"/&gt;</c>.
/// Falls back to the original value when no alias is registered, preserving
/// downstream behavior for primitive encoding types and surfacing diagnostics
/// for unknown identifiers.
/// </summary>
public static string ResolveEncodingType(string encodingType, SchemaContext context)
{
if (string.IsNullOrEmpty(encodingType))
return encodingType;

if (context.EncodingTypeAliases.TryGetValue(encodingType, out var primitive)
&& !string.IsNullOrEmpty(primitive))
{
return primitive;
}

return encodingType;
}

public static string ResolveTypeName(string typeName, SchemaContext context)
{
if (string.IsNullOrEmpty(typeName))
Expand Down
20 changes: 18 additions & 2 deletions src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ internal class TypesCodeGenerator : ICodeGenerator
private static IEnumerable<(string name, string content)> GenerateSet(string ns, SchemaEnumDto enumDto, SchemaContext context, SourceProductionContext sourceContext)
{
var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(context, enumDto.Name, sourceContext);
var encodingTranslated = TypeTranslator.Translate(enumDto.EncodingType);
var resolvedEncoding = TypeResolverHelper.ResolveEncodingType(enumDto.EncodingType, context);
var encodingTranslated = TypeTranslator.Translate(resolvedEncoding);
int maxBitPosition = TypesCatalog.GetPrimitiveLength(encodingTranslated.PrimitiveType) * 8 - 1;

var validChoices = enumDto.Choices
Expand Down Expand Up @@ -98,6 +99,20 @@ internal class TypesCodeGenerator : ICodeGenerator
{
var primitiveTranslated = TypeTranslator.Translate(typeDto.PrimitiveType);

// Register encoding-type alias so that <enum>/<set> referencing this <type> via
// encodingType="<typeDto.Name>" can resolve to the underlying SBE primitive
// (e.g., "uint8EnumEncoding" -> "uint8"). Per SBE 1.0 spec, encodingType is a
// symbolicName_t and may reference any user-declared simple type.
// Only single-element, non-constant aliases qualify: char arrays, constants, and
// composites have non-scalar layouts and are never valid enum/set encodings.
if (!string.IsNullOrEmpty(typeDto.Name)
&& !string.IsNullOrEmpty(typeDto.PrimitiveType)
&& !string.Equals(typeDto.Presence, "constant", System.StringComparison.Ordinal)
&& (string.IsNullOrEmpty(typeDto.Length) || typeDto.Length == "1"))
{
context.EncodingTypeAliases[typeDto.Name] = typeDto.PrimitiveType;
}

if (!TypeTranslator.IsPrimitive(typeDto.Name))
{
var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(context, typeDto.Name, sourceContext);
Expand Down Expand Up @@ -201,7 +216,8 @@ internal class TypesCodeGenerator : ICodeGenerator
private static IEnumerable<(string name, string content)> GenerateEnum(string ns, SchemaEnumDto enumDto, SchemaContext context, SourceProductionContext sourceContext)
{
var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(context, enumDto.Name, sourceContext);
var encodingTranslated = TypeTranslator.Translate(enumDto.EncodingType);
var resolvedEncoding = TypeResolverHelper.ResolveEncodingType(enumDto.EncodingType, context);
var encodingTranslated = TypeTranslator.Translate(resolvedEncoding);

if (!TypesCatalog.HasPrimitiveLength(encodingTranslated.PrimitiveType) && sourceContext.CancellationToken != default)
{
Expand Down
33 changes: 32 additions & 1 deletion src/SbeCodeGenerator/SBESourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ private static void RegisterSourceGeneration(IncrementalGeneratorInitializationC
}

var emittedRuntimeNamespaces = new HashSet<string>(StringComparer.Ordinal);
var emittedHintNames = new HashSet<string>(StringComparer.Ordinal);

foreach (var additionalText in text)
{
Expand Down Expand Up @@ -110,7 +111,37 @@ private static void RegisterSourceGeneration(IncrementalGeneratorInitializationC
try
{
foreach (var item in gen.Generate(ns, schema, context, sourceContext))
sourceContext.AddSource(item.name, item.content);
{
try
{
if (!emittedHintNames.Add(item.name))
{
// Roslyn would throw ArgumentException on duplicate hintName,
// aborting the rest of the phase. Suppress and continue so a
// single duplicate doesn't cascade into thousands of CS0246s
// against partially-emitted files.
if (!sourceContext.CancellationToken.IsCancellationRequested)
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.DuplicateGeneratedSource,
Location.None,
item.name,
phase));
}
continue;
}
sourceContext.AddSource(item.name, item.content);
}
catch (Exception itemEx) when (!sourceContext.CancellationToken.IsCancellationRequested)
{
// Per-item failure must not derail subsequent items in the same phase.
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.MalformedSchema,
Location.None,
path,
$"[{phase}] {item.name}: {itemEx.Message}"));
}
}
}
catch (Exception genEx) when (!sourceContext.CancellationToken.IsCancellationRequested)
{
Expand Down
2 changes: 1 addition & 1 deletion src/SbeCodeGenerator/SbeSourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>SbeSourceGenerator</PackageId>
<Title>SBE Source Generator</Title>
<Version>1.6.0</Version>
<Version>1.6.1</Version>
<Authors>Pedro Sakuma</Authors>
<Company>Pedro Sakuma</Company>
<Product>SBE Source Generator</Product>
Expand Down
10 changes: 10 additions & 0 deletions src/SbeCodeGenerator/SchemaContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public SchemaContext(string schemaKey, HashSet<string>? sharedRuntimeNamespaces
/// </summary>
public Dictionary<string, (string PrimitiveType, string NullValue)> OptionalTypes { get; } = new Dictionary<string, (string, string)>(8);

/// <summary>
/// Maps user-declared simple type names (from &lt;type&gt; elements) to their
/// underlying SBE primitive type name (e.g., "uint8EnumEncoding" -&gt; "uint8").
/// Used to resolve enum/set <c>encodingType</c> attributes that reference a
/// schema-declared alias rather than a primitive directly.
/// Per SBE 1.0 spec, <c>encodingType</c> is a <c>symbolicName_t</c> and may
/// point to either a primitive name or any user-declared &lt;type&gt;.
/// </summary>
public Dictionary<string, string> EncodingTypeAliases { get; } = new Dictionary<string, string>(16);

/// <summary>
/// Tracks composite types and their field types.
/// Maps "CompositeName.FieldName" -> native type (e.g., "GroupSizeEncoding.numInGroup" -> "ushort").
Expand Down
53 changes: 53 additions & 0 deletions tests/SbeCodeGenerator.IntegrationTests/B3EntryPointTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using B3.Entrypoint.Fixp.Sbe.V6;
using Xunit;
using B3Boolean = B3.Entrypoint.Fixp.Sbe.V6.Boolean;

namespace SbeCodeGenerator.IntegrationTests;

/// <summary>
/// End-to-end smoke tests for the B3 Binary EntryPoint v8.4.2 vendor schema.
/// These tests exercise the fixes from issue #164: aliased encodingType resolution
/// (e.g. <c>Boolean : uint8EnumEncoding</c>) and BCL name collisions
/// (the schema declares <c>&lt;enum name="Boolean"&gt;</c>).
/// </summary>
public class B3EntryPointTests
{
[Fact]
public void BooleanEnum_HasByteUnderlyingType()
{
// Validates fix for aliased encodingType: <enum name="Boolean" encodingType="uint8EnumEncoding">
// must resolve uint8EnumEncoding -> byte (not emit "enum Boolean : uint8EnumEncoding").
Assert.Equal(typeof(byte), System.Enum.GetUnderlyingType(typeof(B3Boolean)));
Assert.Equal((byte)0, (byte)B3Boolean.FALSE_VALUE);
Assert.Equal((byte)1, (byte)B3Boolean.TRUE_VALUE);
}

[Fact]
public void FlowTypeEnum_HasByteUnderlyingType()
{
Assert.Equal(typeof(byte), System.Enum.GetUnderlyingType(typeof(FlowType)));
}

[Fact]
public void SequenceMessage_RoundTrip()
{
// Smoke test: encode + decode a message containing a constant valueRef
// (MessageType.Sequence) and a simple field.
SequenceData outbound = new() { NextSeqNo = 42u };

System.Span<byte> buffer = stackalloc byte[SequenceData.MESSAGE_SIZE + 16];
Assert.True(outbound.TryEncode(buffer, out int written));
Assert.True(written > 0);

Assert.True(SequenceData.TryParse(buffer, out SequenceDataReader reader));
Assert.Equal(42u, (uint)reader.Data.NextSeqNo);
Assert.Equal(MessageType.Sequence, SequenceData.MESSAGE_TYPE);
}

[Fact]
public void SchemaConstants_AreExposed()
{
Assert.Equal(9, SequenceData.MESSAGE_ID);
Assert.True(SequenceData.BLOCK_LENGTH > 0);
}
}
Loading
Loading