diff --git a/CHANGELOG.md b/CHANGELOG.md index b31032e..1cb5ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` and `` (#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 `` 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 `` 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 ``. 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 diff --git a/README.md b/README.md index ddc57ce..b39b999 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/src/SbeCodeGenerator/AnalyzerReleases.Shipped.md b/src/SbeCodeGenerator/AnalyzerReleases.Shipped.md index 7257957..b062b71 100644 --- a/src/SbeCodeGenerator/AnalyzerReleases.Shipped.md +++ b/src/SbeCodeGenerator/AnalyzerReleases.Shipped.md @@ -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 diff --git a/src/SbeCodeGenerator/AnalyzerReleases.Unshipped.md b/src/SbeCodeGenerator/AnalyzerReleases.Unshipped.md index 16b554e..e9b1a80 100644 --- a/src/SbeCodeGenerator/AnalyzerReleases.Unshipped.md +++ b/src/SbeCodeGenerator/AnalyzerReleases.Unshipped.md @@ -2,5 +2,3 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------ -SBE013 | SbeSourceGenerator | Warning | Duplicate type name in schema. -SBE014 | SbeSourceGenerator | Warning | sinceVersion exceeds schema version. diff --git a/src/SbeCodeGenerator/Diagnostics/README.md b/src/SbeCodeGenerator/Diagnostics/README.md index 162c8b8..2bfcec2 100644 --- a/src/SbeCodeGenerator/Diagnostics/README.md +++ b/src/SbeCodeGenerator/Diagnostics/README.md @@ -89,6 +89,24 @@ Provides compile-time diagnostics for: **Example**: `Invalid` **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 ``, ``, ``, or `` declarations share the same name within a single schema +**Example**: Two `` blocks in the same `` 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**: `` 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 `` 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: diff --git a/src/SbeCodeGenerator/Diagnostics/SbeDiagnostics.cs b/src/SbeCodeGenerator/Diagnostics/SbeDiagnostics.cs index 1e1eac9..9322bb5 100644 --- a/src/SbeCodeGenerator/Diagnostics/SbeDiagnostics.cs +++ b/src/SbeCodeGenerator/Diagnostics/SbeDiagnostics.cs @@ -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."); } } diff --git a/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs b/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs index 383afab..7305871 100644 --- a/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs +++ b/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs @@ -30,6 +30,30 @@ internal static class TypeResolverHelper public static bool IsDotNetPrimitive(string typeName) => DotNetPrimitiveTypes.Contains(typeName); + /// + /// Resolves an encodingType attribute (from <enum> or <set>) to the + /// underlying SBE primitive type name (e.g., "uint8", "char"). + /// Per SBE 1.0 spec, encodingType is a symbolicName_t and may + /// reference either a primitive type name or a user-declared <type> alias + /// such as <type name="uint8EnumEncoding" primitiveType="uint8"/>. + /// Falls back to the original value when no alias is registered, preserving + /// downstream behavior for primitive encoding types and surfacing diagnostics + /// for unknown identifiers. + /// + 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)) diff --git a/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs b/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs index 441ec69..cc27adf 100644 --- a/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs +++ b/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs @@ -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 @@ -98,6 +99,20 @@ internal class TypesCodeGenerator : ICodeGenerator { var primitiveTranslated = TypeTranslator.Translate(typeDto.PrimitiveType); + // Register encoding-type alias so that / referencing this via + // encodingType="" 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); @@ -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) { diff --git a/src/SbeCodeGenerator/SBESourceGenerator.cs b/src/SbeCodeGenerator/SBESourceGenerator.cs index 5440430..efedd92 100644 --- a/src/SbeCodeGenerator/SBESourceGenerator.cs +++ b/src/SbeCodeGenerator/SBESourceGenerator.cs @@ -61,6 +61,7 @@ private static void RegisterSourceGeneration(IncrementalGeneratorInitializationC } var emittedRuntimeNamespaces = new HashSet(StringComparer.Ordinal); + var emittedHintNames = new HashSet(StringComparer.Ordinal); foreach (var additionalText in text) { @@ -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) { diff --git a/src/SbeCodeGenerator/SbeSourceGenerator.csproj b/src/SbeCodeGenerator/SbeSourceGenerator.csproj index 7018d66..ae5415c 100644 --- a/src/SbeCodeGenerator/SbeSourceGenerator.csproj +++ b/src/SbeCodeGenerator/SbeSourceGenerator.csproj @@ -11,7 +11,7 @@ false SbeSourceGenerator SBE Source Generator - 1.6.0 + 1.6.1 Pedro Sakuma Pedro Sakuma SBE Source Generator diff --git a/src/SbeCodeGenerator/SchemaContext.cs b/src/SbeCodeGenerator/SchemaContext.cs index 776cb56..aa8bbb9 100644 --- a/src/SbeCodeGenerator/SchemaContext.cs +++ b/src/SbeCodeGenerator/SchemaContext.cs @@ -55,6 +55,16 @@ public SchemaContext(string schemaKey, HashSet? sharedRuntimeNamespaces /// public Dictionary OptionalTypes { get; } = new Dictionary(8); + /// + /// Maps user-declared simple type names (from <type> elements) to their + /// underlying SBE primitive type name (e.g., "uint8EnumEncoding" -> "uint8"). + /// Used to resolve enum/set encodingType attributes that reference a + /// schema-declared alias rather than a primitive directly. + /// Per SBE 1.0 spec, encodingType is a symbolicName_t and may + /// point to either a primitive name or any user-declared <type>. + /// + public Dictionary EncodingTypeAliases { get; } = new Dictionary(16); + /// /// Tracks composite types and their field types. /// Maps "CompositeName.FieldName" -> native type (e.g., "GroupSizeEncoding.numInGroup" -> "ushort"). diff --git a/tests/SbeCodeGenerator.IntegrationTests/B3EntryPointTests.cs b/tests/SbeCodeGenerator.IntegrationTests/B3EntryPointTests.cs new file mode 100644 index 0000000..d2f367b --- /dev/null +++ b/tests/SbeCodeGenerator.IntegrationTests/B3EntryPointTests.cs @@ -0,0 +1,53 @@ +using B3.Entrypoint.Fixp.Sbe.V6; +using Xunit; +using B3Boolean = B3.Entrypoint.Fixp.Sbe.V6.Boolean; + +namespace SbeCodeGenerator.IntegrationTests; + +/// +/// 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. Boolean : uint8EnumEncoding) and BCL name collisions +/// (the schema declares <enum name="Boolean">). +/// +public class B3EntryPointTests +{ + [Fact] + public void BooleanEnum_HasByteUnderlyingType() + { + // Validates fix for aliased encodingType: + // 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 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); + } +} diff --git a/tests/SbeCodeGenerator.IntegrationTests/BclCollisionTests.cs b/tests/SbeCodeGenerator.IntegrationTests/BclCollisionTests.cs new file mode 100644 index 0000000..5a0db06 --- /dev/null +++ b/tests/SbeCodeGenerator.IntegrationTests/BclCollisionTests.cs @@ -0,0 +1,51 @@ +using Xunit; + +// NOTE: This test file is intentionally declared INSIDE the schema's namespace. +// Inside an enclosing namespace block, C# name resolution prefers types declared +// in that namespace over types brought in by `using` directives — so `Boolean` +// here unambiguously binds to the user-declared SBE enum, not System.Boolean. +// +// Consumer code that uses `using Bcl.Collision.Test.V0;` from a DIFFERENT +// namespace will see CS0104 ambiguity. That is a language rule, not a generator +// bug; the user should disambiguate via a using-alias (e.g. +// `using Boolean = Bcl.Collision.Test.V0.Boolean;`) or fully qualified names. +namespace Bcl.Collision.Test.V0 +{ + /// + /// Regression test for issue #164 (B3 EntryPoint v8.4.2): + /// proves the GENERATED code for an SBE enum whose name collides with a BCL + /// type (here Boolean) compiles and runs correctly under + /// ImplicitUsings=enable + TreatWarningsAsErrors=true. + /// + public class BclCollisionTests + { + [Fact] + public void BooleanEnum_HasUserDeclaredValues_NotSystemBoolean() + { + Assert.Equal((byte)0, (byte)Boolean.FALSE_VALUE); + Assert.Equal((byte)1, (byte)Boolean.TRUE_VALUE); + } + + [Fact] + public void QuoteRequest_BooleanField_RoundTrips() + { + Span buffer = stackalloc byte[QuoteRequestData.MESSAGE_SIZE]; + ref var msg = ref System.Runtime.CompilerServices.Unsafe.As( + ref System.Runtime.InteropServices.MemoryMarshal.GetReference(buffer)); + msg.IsPrivate = Boolean.TRUE_VALUE; + + Assert.Equal(Boolean.TRUE_VALUE, msg.IsPrivate); + } + + [Fact] + public void QuoteRequest_ConstantFields_BindToUserDeclaredBoolean() + { + // These constants would fail to compile (CS0117) if `Boolean` here + // bound to System.Boolean, since System.Boolean has no + // TRUE_VALUE/FALSE_VALUE members. + Assert.Equal(Boolean.TRUE_VALUE, QuoteRequestData.DEFAULT_PRIVATE); + Assert.Equal(Boolean.FALSE_VALUE, QuoteRequestData.DEFAULT_PUBLIC); + } + } +} + diff --git a/tests/SbeCodeGenerator.IntegrationTests/TestSchemas/b3-entrypoint-messages-8.4.2.xml b/tests/SbeCodeGenerator.IntegrationTests/TestSchemas/b3-entrypoint-messages-8.4.2.xml new file mode 100644 index 0000000..a729053 --- /dev/null +++ b/tests/SbeCodeGenerator.IntegrationTests/TestSchemas/b3-entrypoint-messages-8.4.2.xml @@ -0,0 +1,1573 @@ + + + + + + + 0 + 1 + + + 0 + 2 + + + 8 + + + 8 + + + 0 + + + 0 + 5 + + + 7 + 0 + 5 + 9 + 10 + 11 + 17 + + + 0 + 1 + 2 + 3 + + + 5 + + + T + S + E + B + U + C + + + 6 + + + 2 + 3 + 4 + + + 0 + 1 + + + 0 + 8 + 99 + + + 1 + 5 + 2 + + + 0 + 2 + 3 + 9 + + + 0 + 1 + + + 1 + 105 + 106 + + + 1 + 3 + + + 0 + 8 + X + + + 0 + 1 + 2 + 3 + + + 0 + 3 + 6 + 9 + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + + + 0 + 1 + 2 + 3 + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 20 + 21 + 22 + 23 + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 20 + 21 + 22 + 23 + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 20 + 21 + 23 + 30 + + + 0 + 1 + 2 + 3 + 4 + 5 + 9 + 10 + 11 + + + 0 + 1 + 2 + 3 + + + 1 + 2 + + + 0 + 1 + 3 + 4 + 6 + 7 + A + + + 0 + 3 + 4 + + + 1 + 2 + 3 + 4 + K + W + P + + + 1 + 2 + + + 1 + 2 + + + 1 + 2 + 3 + 4 + + + F + H + + + 0 + 1 + 2 + 4 + 5 + 8 + C + R + Z + + + 203 + + + 202 + 207 + + + 1 + 8 + 100 + 101 + 102 + 103 + 105 + 107 + 200 + 201 + 202 + 203 + 204 + 205 + 206 + 207 + 208 + 209 + 210 + 211 + 212 + 213 + + + 1 + 2 + 3 + + + B + C + D + E + F + G + H + 7 + + + 38 + 39 + + + 0 + 1 + 2 + + + 0 + 1 + + + 4 + 8 + + + 1001 + 1002 + 1003 + + + 1 + 6 + + + 2 + 4 + 17 + 18 + 21 + 101 + + + 2 + 4 + 17 + 18 + 20 + 21 + 101 + 110 + + + 1 + 4 + 7 + 8 + + + 0 + 1 + 2 + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BVMF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -4 + + + + -4 + + + + -8 + + + + -8 + + + + -4 + + + + -8 + + + + -8 + + + + -7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SbeCodeGenerator.IntegrationTests/TestSchemas/bcl-collision-schema.xml b/tests/SbeCodeGenerator.IntegrationTests/TestSchemas/bcl-collision-schema.xml new file mode 100644 index 0000000..406b6e5 --- /dev/null +++ b/tests/SbeCodeGenerator.IntegrationTests/TestSchemas/bcl-collision-schema.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + diff --git a/tests/SbeCodeGenerator.Tests/DuplicateHintNameTests.cs b/tests/SbeCodeGenerator.Tests/DuplicateHintNameTests.cs new file mode 100644 index 0000000..452a0bc --- /dev/null +++ b/tests/SbeCodeGenerator.Tests/DuplicateHintNameTests.cs @@ -0,0 +1,107 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using SbeSourceGenerator; +using System.Collections.Immutable; +using System.Text; +using Xunit; + +namespace SbeCodeGenerator.Tests +{ + /// + /// End-to-end driver tests for the duplicate-hintName guard (SBE015). + /// Exercises the full IIncrementalGenerator pipeline (which is where the + /// guard lives) rather than individual generators. + /// + public class DuplicateHintNameTests + { + private sealed class InMemoryAdditionalText : AdditionalText + { + private readonly SourceText _text; + public InMemoryAdditionalText(string path, string content) + { + Path = path; + _text = SourceText.From(content, Encoding.UTF8); + } + public override string Path { get; } + public override SourceText GetText(System.Threading.CancellationToken cancellationToken = default) => _text; + } + + private static (ImmutableArray Diagnostics, GeneratorDriverRunResult Result) RunGenerator( + params (string path, string content)[] additionalTexts) + { + var compilation = CSharpCompilation.Create( + "TestAssembly", + syntaxTrees: System.Array.Empty(), + references: new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + }); + + var generator = new SBESourceGenerator().AsSourceGenerator(); + var driver = CSharpGeneratorDriver.Create( + generators: new[] { generator }, + additionalTexts: additionalTexts.Select(t => (AdditionalText)new InMemoryAdditionalText(t.path, t.content)).ToImmutableArray()); + + var runResult = driver.RunGenerators(compilation).GetRunResult(); + return (runResult.Diagnostics, runResult); + } + + [Fact] + public void Pipeline_NormalSchema_DoesNotReportDuplicateHintName() + { + var schema = @" + + + + + + + + + + +"; + + var (diagnostics, _) = RunGenerator(("smoke.xml", schema)); + Assert.DoesNotContain(diagnostics, d => d.Id == "SBE015"); + } + + [Fact] + public void Pipeline_DuplicateEnumNamesInSchema_TriggersDuplicateGuard() + { + // Schema with two enums of the same name. Both pass through the + // generator (RegisterGeneratedTypeName tolerates the second with + // SBE013), producing two items with identical hintName. Without + // SBE015, AddSource would throw and abort the entire 'types' phase. + var schema = @" + + + + + + + + + + + 1 + 2 + + + 1 + 2 + + +"; + + var (diagnostics, result) = RunGenerator(("dup.xml", schema)); + + Assert.All(result.Results, r => Assert.Null(r.Exception)); + Assert.Contains(diagnostics, d => d.Id == "SBE015"); + } + } +} diff --git a/tests/SbeCodeGenerator.Tests/TypesCodeGeneratorTests.cs b/tests/SbeCodeGenerator.Tests/TypesCodeGeneratorTests.cs index cfc3b3b..6b2ad88 100644 --- a/tests/SbeCodeGenerator.Tests/TypesCodeGeneratorTests.cs +++ b/tests/SbeCodeGenerator.Tests/TypesCodeGeneratorTests.cs @@ -1261,5 +1261,116 @@ public void Generate_WithMonthYearNoDayComposite_DefaultsDay() Assert.Contains("(ushort?)Year is { } y", toDateOnlyResult.content); Assert.Contains("(byte?)Month is { } m", toDateOnlyResult.content); } + + [Fact] + public void Generate_EnumWithAliasedEncodingType_ResolvesUnderlyingPrimitive() + { + // SBE 1.0 spec: is symbolicName_t and may + // reference a user-declared alias whose primitiveType supplies + // the underlying wire type. The advisory presence/semanticType/nullValue + // on the alias must NOT leak into the generated enum. + var generator = new TypesCodeGenerator(); + var context = new SchemaContext("test-schema"); + var schema = SchemaReader.Parse(@" + + + + + + 1 + 2 + + + 1 + + + "); + + var results = generator.Generate("TestNamespace", schema, context, default(SourceProductionContext)).ToList(); + + var sideEnum = results.First(r => r.name.Contains("Side")).content; + Assert.Contains("public enum Side : byte", sideEnum); + Assert.DoesNotContain("uint8EnumEncoding", sideEnum); + + var execEnum = results.First(r => r.name.Contains("ExecType")).content; + Assert.Contains("public enum ExecType : ushort", execEnum); + Assert.DoesNotContain("uint16EnumEncoding", execEnum); + } + + [Fact] + public void Generate_SetWithAliasedEncodingType_ResolvesUnderlyingPrimitive() + { + var generator = new TypesCodeGenerator(); + var context = new SchemaContext("test-schema"); + var schema = SchemaReader.Parse(@" + + + + + 0 + 1 + + + "); + + var results = generator.Generate("TestNamespace", schema, context, default(SourceProductionContext)).ToList(); + var setResult = results.First(r => r.name.Contains("Flags")).content; + Assert.Contains("public enum Flags : byte", setResult); + Assert.DoesNotContain("uint8SetEncoding", setResult); + } + + [Fact] + public void Generate_EnumWithCharAliasedEncodingType_ResolvesToChar() + { + // Char enum encoding via alias must still produce a byte-backed enum + // (consistent with non-aliased char encoding) and emit (byte)'X' literals. + var generator = new TypesCodeGenerator(); + var context = new SchemaContext("test-schema"); + var schema = SchemaReader.Parse(@" + + + + + 0 + 2 + + + "); + + var results = generator.Generate("TestNamespace", schema, context, default(SourceProductionContext)).ToList(); + var enumContent = results.First(r => r.name.Contains("OrdStatus")).content; + Assert.Contains("public enum OrdStatus : byte", enumContent); + Assert.DoesNotContain("charEncoding", enumContent); + Assert.Contains("(byte)'0'", enumContent); + Assert.Contains("(byte)'2'", enumContent); + } + + [Fact] + public void Generate_EnumWithFixedSizeCharAlias_DoesNotResolveAsEncoding() + { + // length>1 makes the alias a char array (FixedSizeCharType), not a scalar + // encoding alias. We must not register it as an enum encoding alias — + // otherwise an enum referencing it would silently emit invalid code. + var generator = new TypesCodeGenerator(); + var context = new SchemaContext("test-schema"); + SchemaReader.Parse(@" + + + + + "); + + // Generate to populate the context + var schema = SchemaReader.Parse(@" + + + + + "); + generator.Generate("TestNamespace", schema, context, default(SourceProductionContext)).ToList(); + + Assert.False(context.EncodingTypeAliases.ContainsKey("Symbol8"), + "Fixed-size char arrays must not be registered as encoding-type aliases."); + } } }