diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08316ad..e5b7bbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Per-schema incremental invalidation for XML AdditionalFiles (#170)**: `SBESourceGenerator` no longer `.Collect()`s the entire schema set before generation. Each schema now flows through its own `RegisterSourceOutput` path, so editing one XML file only re-runs the generators for that schema. Runtime helpers (`SpanReader` / `SpanWriter`) are emitted in a separate namespace-deduplicated step to preserve shared helper generation without reintroducing whole-project schema invalidation.
+### Fixed
+
+- **Accurate XML locations for schema diagnostics (#171)**: schema diagnostics now point at the offending XML element or attribute when line/column information is available from the additional-file source text, instead of reporting `Location.None`. Diagnostics still fall back to `Location.None` for cases where a reliable XML location cannot be determined.
+
## [1.7.0] - 2026-04-30
### Added
diff --git a/src/SbeCodeGenerator/Diagnostics/README.md b/src/SbeCodeGenerator/Diagnostics/README.md
index 5b7a05d..294abcb 100644
--- a/src/SbeCodeGenerator/Diagnostics/README.md
+++ b/src/SbeCodeGenerator/Diagnostics/README.md
@@ -133,7 +133,8 @@ Diagnostics are automatically reported during source generation. When you build
## Implementation Notes
-- Diagnostics use `Location.None` as source generators don't have access to the original XML file locations
+- Diagnostics now use `Location.Create(path, textSpan, lineSpan)` for XML additional files whenever the generator can determine the offending schema node or attribute position from the parsed `XmlReader` line info and the `AdditionalText` source text
+- Diagnostics still fall back to `Location.None` when no reliable source position exists (for example, some malformed-schema failures or diagnostics originating from non-XML inputs such as invalid MSBuild properties)
- The generator gracefully handles errors by using fallback values and continuing generation
- Each generator phase (Types, Messages, Utilities, Validation) runs in isolation — a failure in one phase does not block the others
- Test code uses `default(SourceProductionContext)` which has special handling to skip diagnostic reporting
diff --git a/src/SbeCodeGenerator/Diagnostics/XmlDiagnosticLocation.cs b/src/SbeCodeGenerator/Diagnostics/XmlDiagnosticLocation.cs
new file mode 100644
index 0000000..0cf4a50
--- /dev/null
+++ b/src/SbeCodeGenerator/Diagnostics/XmlDiagnosticLocation.cs
@@ -0,0 +1,55 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+using System;
+using System.Xml;
+
+namespace SbeSourceGenerator.Diagnostics
+{
+ ///
+ /// Creates Roslyn instances for XML additional files.
+ ///
+ internal static class XmlDiagnosticLocation
+ {
+ public static Location Create(SourceText? sourceText, string? filePath, int lineNumber, int linePosition, int width = 1)
+ {
+ if (sourceText == null || string.IsNullOrWhiteSpace(filePath))
+ return Location.None;
+
+ string safeFilePath = filePath!;
+
+ if (lineNumber <= 0 || linePosition <= 0)
+ return Location.None;
+
+ int lineIndex = lineNumber - 1;
+ if (lineIndex >= sourceText.Lines.Count)
+ return Location.None;
+
+ var line = sourceText.Lines[lineIndex];
+ int columnIndex = Math.Max(0, Math.Min(linePosition - 1, line.Span.Length));
+ int start = line.Start + columnIndex;
+ int remaining = line.End - start;
+ int safeWidth = remaining == 0 ? 0 : Math.Max(1, Math.Min(width, remaining));
+
+ var span = new TextSpan(start, safeWidth);
+ var startPosition = new LinePosition(lineIndex, columnIndex);
+ var endPosition = new LinePosition(lineIndex, columnIndex + safeWidth);
+ return Location.Create(safeFilePath, span, new LinePositionSpan(startPosition, endPosition));
+ }
+
+ public static Location CreateFromLineInfo(SourceText? sourceText, string? filePath, IXmlLineInfo lineInfo, int width = 1)
+ {
+ if (lineInfo == null || !lineInfo.HasLineInfo())
+ return Location.None;
+
+ return Create(sourceText, filePath, lineInfo.LineNumber, lineInfo.LinePosition, width);
+ }
+
+ public static Location CreateFromException(SourceText? sourceText, string? filePath, XmlException exception)
+ {
+ if (exception == null)
+ throw new ArgumentNullException(nameof(exception));
+
+ return Create(sourceText, filePath, exception.LineNumber, exception.LinePosition);
+ }
+ }
+}
diff --git a/src/SbeCodeGenerator/Generators/MessagesCodeGenerator.cs b/src/SbeCodeGenerator/Generators/MessagesCodeGenerator.cs
index 411b31d..b55464b 100644
--- a/src/SbeCodeGenerator/Generators/MessagesCodeGenerator.cs
+++ b/src/SbeCodeGenerator/Generators/MessagesCodeGenerator.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using SbeSourceGenerator.Diagnostics;
using SbeSourceGenerator.Generators.Fields;
+using SbeSourceGenerator.Helpers;
using SbeSourceGenerator.Schema;
using SbeSourceGenerator.SemanticTypes;
using System.Collections.Generic;
@@ -21,7 +22,11 @@ internal class MessagesCodeGenerator : ICodeGenerator
foreach (var messageDto in schema.Messages)
{
- var generatedMessageName = TypeResolverHelper.RegisterGeneratedTypeName(context, messageDto.Name, sourceContext);
+ var generatedMessageName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ messageDto.Name,
+ sourceContext,
+ messageDto.Source.GetAttributeOrElement("name"));
var versions = GetMessageVersions(messageDto, schemaVersion, sourceContext);
var baseNamespace = StripSchemaVersion(ns);
@@ -162,11 +167,11 @@ private static List GetMessageVersions(SchemaMessageDto messageDto, int sch
{
if (int.TryParse(field.SinceVersion, out int sinceVersion))
{
- if (schemaVersion >= 0 && sinceVersion > schemaVersion && sourceContext.CancellationToken != default)
+ if (schemaVersion >= 0 && sinceVersion > schemaVersion && sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.SinceVersionExceedsSchemaVersion,
- Location.None,
+ field.Source.GetAttributeOrElement("sinceVersion"),
field.Name,
sinceVersion.ToString(),
schemaVersion.ToString()));
@@ -177,11 +182,11 @@ private static List GetMessageVersions(SchemaMessageDto messageDto, int sch
versions.Add(v);
}
}
- else if (sourceContext.CancellationToken != default)
+ else if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidIntegerAttribute,
- Location.None,
+ field.Source.GetAttributeOrElement("sinceVersion"),
"sinceVersion",
field.SinceVersion,
field.Name));
@@ -195,11 +200,11 @@ private static List GetMessageVersions(SchemaMessageDto messageDto, int sch
{
if (int.TryParse(data.SinceVersion, out int sinceVersion))
{
- if (schemaVersion >= 0 && sinceVersion > schemaVersion && sourceContext.CancellationToken != default)
+ if (schemaVersion >= 0 && sinceVersion > schemaVersion && sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.SinceVersionExceedsSchemaVersion,
- Location.None,
+ data.Source.GetAttributeOrElement("sinceVersion"),
data.Name,
sinceVersion.ToString(),
schemaVersion.ToString()));
@@ -208,11 +213,11 @@ private static List GetMessageVersions(SchemaMessageDto messageDto, int sch
for (int v = 0; v <= sinceVersion; v++)
versions.Add(v);
}
- else if (sourceContext.CancellationToken != default)
+ else if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidIntegerAttribute,
- Location.None,
+ data.Source.GetAttributeOrElement("sinceVersion"),
"sinceVersion",
data.SinceVersion,
data.Name));
@@ -318,7 +323,7 @@ private static List GetFieldsForVersion(
field.Id,
resolvedType,
field.Description,
- ParseOffset(field.Offset, field.Name, sourceContext),
+ ParseOffset(field.Offset, field.Name, sourceContext, field.Source.GetAttributeOrElement("offset")),
TypeResolverHelper.GetTypeLength(field.Type, context),
field.SinceVersion,
field.Deprecated,
@@ -354,7 +359,7 @@ private static List GetFieldsForVersion(
fieldType,
effectivePrimitiveType,
field.Description,
- ParseOffset(field.Offset, field.Name, sourceContext),
+ ParseOffset(field.Offset, field.Name, sourceContext, field.Source.GetAttributeOrElement("offset")),
TypeResolverHelper.GetTypeLength(field.Type, context),
field.SinceVersion,
field.Deprecated,
@@ -372,7 +377,7 @@ private static List GetFieldsForVersion(
field.Id,
resolvedType,
field.Description,
- ParseOffset(field.Offset, field.Name, sourceContext),
+ ParseOffset(field.Offset, field.Name, sourceContext, field.Source.GetAttributeOrElement("offset")),
TypeResolverHelper.GetTypeLength(field.Type, context),
field.SinceVersion,
field.Deprecated,
@@ -429,7 +434,7 @@ private static void TryAppendSemanticAccessor(
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.SemanticConverterWireMismatch,
- registration.Location ?? Location.None,
+ field.Source.GetAttributeOrElement("semanticType"),
registration.ConverterFullyQualifiedName,
registration.SemanticType,
PrimitiveSpecialTypeMap.ToCSharpKeyword(registration.WireSpecialType),
@@ -454,7 +459,7 @@ private static void TryAppendSemanticAccessor(
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.SemanticAccessorNameCollision,
- registration.Location ?? Location.None,
+ field.Source.GetAttributeOrElement("semanticType"),
generatedFieldName,
"",
registration.SemanticType));
@@ -508,7 +513,11 @@ private static List BuildGroups(
var result = new List(groups.Count);
foreach (var group in groups)
{
- var groupName = TypeResolverHelper.RegisterGeneratedTypeName(context, group.Name, sourceContext);
+ var groupName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ group.Name,
+ sourceContext,
+ group.Source.GetAttributeOrElement("name"));
var groupFields = new List(group.Fields.Count);
foreach (var field in group.Fields)
@@ -522,7 +531,7 @@ private static List BuildGroups(
field.Id,
resolvedFieldType,
field.Description,
- ParseOffset(field.Offset, field.Name, sourceContext),
+ ParseOffset(field.Offset, field.Name, sourceContext, field.Source.GetAttributeOrElement("offset")),
TypeResolverHelper.GetTypeLength(field.Type, context),
field.SinceVersion,
field.Deprecated,
@@ -533,7 +542,9 @@ private static List BuildGroups(
}
var groupConstants = BuildConstants(group.Constants, context);
- var numInGroupType = TypeResolverHelper.ResolveTypeName(GetNumInGroupType(group.DimensionType, context, sourceContext), context);
+ var numInGroupType = TypeResolverHelper.ResolveTypeName(
+ GetNumInGroupType(group.DimensionType, group.Source, context, sourceContext),
+ context);
result.Add(new GroupDefinition(
versionNamespace,
@@ -600,7 +611,7 @@ private static List GetDataForVersion(List
return null;
}
- private static string GetNumInGroupType(string dimensionType, SchemaContext context, SourceProductionContext sourceContext = default)
+ private static string GetNumInGroupType(string dimensionType, SchemaSourceInfo source, SchemaContext context, SourceProductionContext sourceContext = default)
{
var key = $"{dimensionType}.numInGroup";
if (context.CompositeFieldTypes.TryGetValue(key, out string? numInGroupType))
@@ -611,7 +622,7 @@ private static string GetNumInGroupType(string dimensionType, SchemaContext cont
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.UnsupportedConstruct,
- Location.None,
+ source.GetAttributeOrElement("dimensionType"),
"dimensionType",
dimensionType,
$"Composite type '{dimensionType}' not found. Falling back to ushort for numInGroup."));
@@ -619,7 +630,7 @@ private static string GetNumInGroupType(string dimensionType, SchemaContext cont
return "ushort";
}
- private static int? ParseOffset(string offset, string fieldName, SourceProductionContext sourceContext)
+ private static int? ParseOffset(string offset, string fieldName, SourceProductionContext sourceContext, Location location)
{
if (string.IsNullOrEmpty(offset))
return null;
@@ -628,12 +639,12 @@ private static string GetNumInGroupType(string dimensionType, SchemaContext cont
return result;
// Only report diagnostic if context has a valid CancellationToken (not default)
- if (sourceContext.CancellationToken != default)
+ if (sourceContext.CanReportDiagnostics())
{
// Report diagnostic for invalid offset
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidIntegerAttribute,
- Location.None,
+ location,
"offset",
offset,
fieldName));
diff --git a/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs b/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs
index 7305871..984a0ca 100644
--- a/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs
+++ b/src/SbeCodeGenerator/Generators/TypeResolverHelper.cs
@@ -96,7 +96,12 @@ public static string NormalizeValueRef(string valueRef, SchemaContext context)
return string.Concat(normalizedType, separator, remainder);
}
- public static int GetTypeLength(string type, SchemaContext context, SourceProductionContext sourceContext = default, string elementName = "")
+ public static int GetTypeLength(
+ string type,
+ SchemaContext context,
+ SourceProductionContext sourceContext = default,
+ string elementName = "",
+ Location? location = null)
{
if (TypesCatalog.PrimitiveTypeLengths.TryGetValue(type, out int length))
return length;
@@ -112,14 +117,18 @@ public static int GetTypeLength(string type, SchemaContext context, SourceProduc
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.UnresolvedTypeReference,
- Location.None,
+ location ?? Location.None,
type,
elementName));
}
return 0;
}
- public static string RegisterGeneratedTypeName(SchemaContext context, string originalName, SourceProductionContext sourceContext = default)
+ public static string RegisterGeneratedTypeName(
+ SchemaContext context,
+ string originalName,
+ SourceProductionContext sourceContext = default,
+ Location? location = null)
{
if (string.IsNullOrEmpty(originalName))
return originalName;
@@ -132,7 +141,7 @@ public static string RegisterGeneratedTypeName(SchemaContext context, string ori
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.DuplicateTypeName,
- Location.None,
+ location ?? Location.None,
originalName));
}
}
diff --git a/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs b/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs
index a63700f..3b88c52 100644
--- a/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs
+++ b/src/SbeCodeGenerator/Generators/TypesCodeGenerator.cs
@@ -41,7 +41,11 @@ 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 generatedName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ enumDto.Name,
+ sourceContext,
+ enumDto.Source.GetAttributeOrElement("name"));
var resolvedEncoding = TypeResolverHelper.ResolveEncodingType(enumDto.EncodingType, context);
var encodingTranslated = TypeTranslator.Translate(resolvedEncoding);
int maxBitPosition = TypesCatalog.GetPrimitiveLength(encodingTranslated.PrimitiveType) * 8 - 1;
@@ -49,18 +53,22 @@ internal class TypesCodeGenerator : ICodeGenerator
var validChoices = enumDto.Choices
.Select(choice =>
{
- var parsedValue = XmlParsingHelpers.ParseEnumFlagValue(choice.InnerText, choice.Name, sourceContext);
+ var parsedValue = XmlParsingHelpers.ParseEnumFlagValue(
+ choice.InnerText,
+ choice.Name,
+ sourceContext,
+ choice.Source.ElementLocation);
return new { choice, parsedValue };
})
.Where(x =>
{
if (x.parsedValue.HasValue && x.parsedValue.Value > maxBitPosition)
{
- if (sourceContext.CancellationToken != default)
+ if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.SetChoiceExceedsBitWidth,
- Location.None,
+ x.choice.Source.ElementLocation,
x.choice.Name,
enumDto.Name,
x.parsedValue.Value,
@@ -122,17 +130,21 @@ internal class TypesCodeGenerator : ICodeGenerator
if (!TypeTranslator.IsPrimitive(typeDto.Name))
{
- var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(context, typeDto.Name, sourceContext);
+ var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ typeDto.Name,
+ sourceContext,
+ typeDto.Source.GetAttributeOrElement("name"));
int lengthValue = 0;
if (!string.IsNullOrEmpty(typeDto.Length))
{
if (!int.TryParse(typeDto.Length, out lengthValue))
{
- if (sourceContext.CancellationToken != default)
+ if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidIntegerAttribute,
- Location.None,
+ typeDto.Source.GetAttributeOrElement("length"),
"length",
typeDto.Length,
"type"));
@@ -166,7 +178,12 @@ internal class TypesCodeGenerator : ICodeGenerator
TypeResolverHelper.ResolveTypeName(nativeType, context),
typeDto.SemanticType,
typeDto.NullValue,
- TypeResolverHelper.GetTypeLength(nativeType, context)
+ TypeResolverHelper.GetTypeLength(
+ nativeType,
+ context,
+ sourceContext,
+ typeDto.Name,
+ typeDto.Source.GetAttributeOrElement("primitiveType"))
),
_ => new TypeDefinition(
ns,
@@ -174,7 +191,12 @@ internal class TypesCodeGenerator : ICodeGenerator
typeDto.Description,
TypeResolverHelper.ResolveTypeName(nativeType, context),
typeDto.SemanticType,
- TypeResolverHelper.GetTypeLength(nativeType, context),
+ TypeResolverHelper.GetTypeLength(
+ nativeType,
+ context,
+ sourceContext,
+ typeDto.Name,
+ typeDto.Source.GetAttributeOrElement("primitiveType")),
typeDto.MinValue,
typeDto.MaxValue
)
@@ -189,11 +211,11 @@ internal class TypesCodeGenerator : ICodeGenerator
context.OptionalTypes[typeDto.Name] = (nativeType, typeDto.NullValue);
var resolvedType = TypeResolverHelper.ResolveTypeName(nativeType, context);
if (string.IsNullOrEmpty(typeDto.NullValue) && !TypesCatalog.HasNullValue(resolvedType)
- && sourceContext.CancellationToken != default)
+ && sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.UnknownPrimitiveTypeFallback,
- Location.None,
+ typeDto.Source.GetAttributeOrElement("primitiveType"),
resolvedType, "null sentinel", typeDto.Name));
}
}
@@ -223,15 +245,19 @@ 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 generatedName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ enumDto.Name,
+ sourceContext,
+ enumDto.Source.GetAttributeOrElement("name"));
var resolvedEncoding = TypeResolverHelper.ResolveEncodingType(enumDto.EncodingType, context);
var encodingTranslated = TypeTranslator.Translate(resolvedEncoding);
- if (!TypesCatalog.HasPrimitiveLength(encodingTranslated.PrimitiveType) && sourceContext.CancellationToken != default)
+ if (!TypesCatalog.HasPrimitiveLength(encodingTranslated.PrimitiveType) && sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.UnknownPrimitiveTypeFallback,
- Location.None,
+ enumDto.Source.GetAttributeOrElement("encodingType"),
encodingTranslated.PrimitiveType, "length", enumDto.Name));
}
@@ -303,7 +329,11 @@ internal class TypesCodeGenerator : ICodeGenerator
}
}
- var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(context, compositeDto.Name, sourceContext);
+ var generatedName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ compositeDto.Name,
+ sourceContext,
+ compositeDto.Source.GetAttributeOrElement("name"));
// Separate ref fields from primitive fields
var refFields = compositeDto.Fields
@@ -325,11 +355,11 @@ internal class TypesCodeGenerator : ICodeGenerator
if (ft.Field.Presence == "optional" && string.IsNullOrEmpty(ft.Field.NullValue))
{
var resolvedType = TypeResolverHelper.ResolveTypeName(ft.Translation.PrimitiveType, context);
- if (!TypesCatalog.HasNullValue(resolvedType) && sourceContext.CancellationToken != default)
+ if (!TypesCatalog.HasNullValue(resolvedType) && sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.UnknownPrimitiveTypeFallback,
- Location.None,
+ ft.Field.Source.GetAttributeOrElement("primitiveType"),
resolvedType, "null sentinel", $"{compositeDto.Name}.{ft.Field.Name}"));
}
}
@@ -349,7 +379,11 @@ internal class TypesCodeGenerator : ICodeGenerator
if (ft.Translation.PrimitiveType == "char" && ft.Field.Presence != "constant"
&& int.TryParse(ft.Field.Length, out var charLen) && charLen > 1)
{
- var charTypeName = TypeResolverHelper.RegisterGeneratedTypeName(context, ft.Field.Name, sourceContext);
+ var charTypeName = TypeResolverHelper.RegisterGeneratedTypeName(
+ context,
+ ft.Field.Name,
+ sourceContext,
+ ft.Field.Source.GetAttributeOrElement("name"));
var charTypeDef = new FixedSizeCharTypeDefinition(ns, charTypeName, ft.Field.Description, charLen, ft.Field.CharacterEncoding);
context.CustomTypeLengths[ft.Field.Name] = charLen;
context.StructTypeNames.Add(ft.Field.Name);
diff --git a/src/SbeCodeGenerator/Generators/ValidationGenerator.cs b/src/SbeCodeGenerator/Generators/ValidationGenerator.cs
index 79b46e9..ab8222d 100644
--- a/src/SbeCodeGenerator/Generators/ValidationGenerator.cs
+++ b/src/SbeCodeGenerator/Generators/ValidationGenerator.cs
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using SbeSourceGenerator.Diagnostics;
+using SbeSourceGenerator.Helpers;
using SbeSourceGenerator.Schema;
using System.Collections.Generic;
using System.Linq;
@@ -49,11 +50,11 @@ internal class ValidationGenerator : ICodeGenerator
bool valid = true;
if (!string.IsNullOrEmpty(field.MinValue) && !double.TryParse(field.MinValue, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out _))
{
- if (sourceContext.CancellationToken != default)
+ if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidNumericConstraint,
- Location.None,
+ field.Source.GetAttributeOrElement("minValue"),
"minValue",
field.MinValue,
field.Name));
@@ -62,11 +63,11 @@ internal class ValidationGenerator : ICodeGenerator
}
if (!string.IsNullOrEmpty(field.MaxValue) && !double.TryParse(field.MaxValue, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out _))
{
- if (sourceContext.CancellationToken != default)
+ if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidNumericConstraint,
- Location.None,
+ field.Source.GetAttributeOrElement("maxValue"),
"maxValue",
field.MaxValue,
field.Name));
diff --git a/src/SbeCodeGenerator/Helpers/SourceProductionContextExtensions.cs b/src/SbeCodeGenerator/Helpers/SourceProductionContextExtensions.cs
new file mode 100644
index 0000000..bbfb3a8
--- /dev/null
+++ b/src/SbeCodeGenerator/Helpers/SourceProductionContextExtensions.cs
@@ -0,0 +1,15 @@
+using Microsoft.CodeAnalysis;
+
+namespace SbeSourceGenerator.Helpers
+{
+ ///
+ /// Helpers for safely reporting diagnostics from optional/default source-production contexts.
+ ///
+ internal static class SourceProductionContextExtensions
+ {
+ public static bool CanReportDiagnostics(this SourceProductionContext context)
+ {
+ return !context.Equals(default(SourceProductionContext));
+ }
+ }
+}
diff --git a/src/SbeCodeGenerator/Helpers/XmlParsingHelpers.cs b/src/SbeCodeGenerator/Helpers/XmlParsingHelpers.cs
index 16c02f3..d195e9e 100644
--- a/src/SbeCodeGenerator/Helpers/XmlParsingHelpers.cs
+++ b/src/SbeCodeGenerator/Helpers/XmlParsingHelpers.cs
@@ -114,7 +114,7 @@ public static string GetInnerTextOrEmpty(this XmlElement element)
/// Gets an integer attribute value from an XmlElement. Returns null if attribute doesn't exist or is empty.
/// Emits a diagnostic if the value cannot be parsed as an integer.
///
- public static int? GetIntAttributeOrNull(this XmlElement element, string attributeName, SourceProductionContext context)
+ public static int? GetIntAttributeOrNull(this XmlElement element, string attributeName, SourceProductionContext context, Location? location = null)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
@@ -127,11 +127,11 @@ public static string GetInnerTextOrEmpty(this XmlElement element)
return result;
// Only report diagnostic if context has a valid CancellationToken (not default)
- if (context.CancellationToken != default)
+ if (context.CanReportDiagnostics())
{
context.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidIntegerAttribute,
- Location.None,
+ location ?? Location.None,
attributeName,
value,
element.Name));
@@ -144,7 +144,7 @@ public static string GetInnerTextOrEmpty(this XmlElement element)
/// Gets an integer attribute value from an XmlElement with a fallback default value.
/// Emits a diagnostic if the value cannot be parsed as an integer.
///
- public static int GetIntAttributeOrDefault(this XmlElement element, string attributeName, int defaultValue, SourceProductionContext context)
+ public static int GetIntAttributeOrDefault(this XmlElement element, string attributeName, int defaultValue, SourceProductionContext context, Location? location = null)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
@@ -157,11 +157,11 @@ public static int GetIntAttributeOrDefault(this XmlElement element, string attri
return result;
// Only report diagnostic if context has a valid CancellationToken (not default)
- if (context.CancellationToken != default)
+ if (context.CanReportDiagnostics())
{
context.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidIntegerAttribute,
- Location.None,
+ location ?? Location.None,
attributeName,
value,
element.Name));
@@ -174,7 +174,7 @@ public static int GetIntAttributeOrDefault(this XmlElement element, string attri
/// Gets an attribute value from an XmlElement and validates it's not empty.
/// Emits a diagnostic if the attribute is missing or empty.
///
- public static string GetRequiredAttribute(this XmlElement element, string attributeName, SourceProductionContext context)
+ public static string GetRequiredAttribute(this XmlElement element, string attributeName, SourceProductionContext context, Location? location = null)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
@@ -183,11 +183,11 @@ public static string GetRequiredAttribute(this XmlElement element, string attrib
if (string.IsNullOrEmpty(value))
{
// Only report diagnostic if context has a valid CancellationToken (not default)
- if (context.CancellationToken != default)
+ if (context.CanReportDiagnostics())
{
context.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.MissingRequiredAttribute,
- Location.None,
+ location ?? Location.None,
attributeName,
element.Name));
}
@@ -202,7 +202,7 @@ public static string GetRequiredAttribute(this XmlElement element, string attrib
/// Safely parses an integer value for enum flag bit-shifting operations.
/// Emits a diagnostic if the value cannot be parsed.
///
- public static int? ParseEnumFlagValue(string value, string fieldName, SourceProductionContext context)
+ public static int? ParseEnumFlagValue(string value, string fieldName, SourceProductionContext context, Location? location = null)
{
if (string.IsNullOrEmpty(value))
return null;
@@ -211,11 +211,11 @@ public static string GetRequiredAttribute(this XmlElement element, string attrib
return result;
// Only report diagnostic if context has a valid CancellationToken (not default)
- if (context.CancellationToken != default)
+ if (context.CanReportDiagnostics())
{
context.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.InvalidEnumFlagValue,
- Location.None,
+ location ?? Location.None,
fieldName,
value));
}
diff --git a/src/SbeCodeGenerator/SBESourceGenerator.cs b/src/SbeCodeGenerator/SBESourceGenerator.cs
index efcbfe1..ac11408 100644
--- a/src/SbeCodeGenerator/SBESourceGenerator.cs
+++ b/src/SbeCodeGenerator/SBESourceGenerator.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
using SbeSourceGenerator.Diagnostics;
using SbeSourceGenerator.Generators;
using SbeSourceGenerator.Schema;
@@ -13,6 +14,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Threading;
+using System.Xml;
namespace SbeSourceGenerator
{
@@ -101,13 +103,15 @@ private static void RegisterSourceGeneration(IncrementalGeneratorInitializationC
var emittedHintNames = new HashSet(StringComparer.Ordinal);
+ SourceText? sourceText = null;
try
{
- var xmlContent = additionalText.GetText(sourceContext.CancellationToken)?.ToString();
+ sourceText = additionalText.GetText(sourceContext.CancellationToken);
+ var xmlContent = sourceText?.ToString();
if (string.IsNullOrEmpty(xmlContent))
return;
- var schema = SchemaReader.Parse(xmlContent!, sourceContext);
+ var schema = SchemaReader.Parse(xmlContent!, sourceContext, path, sourceText);
string ns = GetNamespaceFromSchema(schema, path);
string schemaKey = CreateSchemaKey(path);
@@ -122,7 +126,7 @@ private static void RegisterSourceGeneration(IncrementalGeneratorInitializationC
}
context.EndianConversion = ComputeEndianConversion(
- context.ByteOrder, hostHint, sourceContext, path);
+ context.ByteOrder, hostHint, sourceContext, schema, path);
if (!string.IsNullOrEmpty(schema.HeaderType))
context.HeaderType = schema.HeaderType;
@@ -191,9 +195,12 @@ private static void RegisterSourceGeneration(IncrementalGeneratorInitializationC
{
if (!sourceContext.CancellationToken.IsCancellationRequested)
{
+ var location = ex is XmlException xmlException
+ ? XmlDiagnosticLocation.CreateFromException(sourceText, additionalText.Path, xmlException)
+ : Location.None;
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.MalformedSchema,
- Location.None,
+ location,
additionalText.Path,
ex.Message));
}
@@ -286,7 +293,6 @@ private static string CreateSchemaKey(string path)
return string.Concat(sanitized.ToString(), "_", hash);
}
-
private static string GetNamespaceFromSchema(ParsedSchema schema, string path)
{
var baseNamespaceFromPath = GetNamespaceFromPath(path);
@@ -406,8 +412,12 @@ private static string NormalizeIdentifier(string value)
///
/// Computes the endian conversion strategy from schema byteOrder and optional host hint.
///
- private static EndianConversion ComputeEndianConversion(string schemaByteOrder, string? hostHint,
- SourceProductionContext sourceContext, string schemaPath)
+ private static EndianConversion ComputeEndianConversion(
+ string schemaByteOrder,
+ string? hostHint,
+ SourceProductionContext sourceContext,
+ ParsedSchema schema,
+ string schemaPath)
{
bool isBigEndianSchema = schemaByteOrder.Equals("bigEndian", StringComparison.OrdinalIgnoreCase);
@@ -421,7 +431,7 @@ private static EndianConversion ComputeEndianConversion(string schemaByteOrder,
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.NonNativeByteOrder,
- Location.None,
+ schema.Source.GetAttributeOrElement("byteOrder"),
schemaPath,
schemaByteOrder));
}
diff --git a/src/SbeCodeGenerator/Schema/ParsedSchema.cs b/src/SbeCodeGenerator/Schema/ParsedSchema.cs
index e594f02..b76e115 100644
--- a/src/SbeCodeGenerator/Schema/ParsedSchema.cs
+++ b/src/SbeCodeGenerator/Schema/ParsedSchema.cs
@@ -20,5 +20,8 @@ internal record ParsedSchema(
List Sets,
List Messages,
string HeaderType = "messageHeader"
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaCompositeDto.cs b/src/SbeCodeGenerator/Schema/SchemaCompositeDto.cs
index 30d3322..69a12b1 100644
--- a/src/SbeCodeGenerator/Schema/SchemaCompositeDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaCompositeDto.cs
@@ -12,5 +12,8 @@ internal record SchemaCompositeDto(
List Fields,
List? NestedComposites = null,
List? NestedEnums = null
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaDataDto.cs b/src/SbeCodeGenerator/Schema/SchemaDataDto.cs
index 97a8594..a743a20 100644
--- a/src/SbeCodeGenerator/Schema/SchemaDataDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaDataDto.cs
@@ -9,5 +9,8 @@ internal record SchemaDataDto(
string Type,
string Description,
string SinceVersion = ""
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaEnumDto.cs b/src/SbeCodeGenerator/Schema/SchemaEnumDto.cs
index 98c74b7..b16730b 100644
--- a/src/SbeCodeGenerator/Schema/SchemaEnumDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaEnumDto.cs
@@ -11,5 +11,8 @@ internal record SchemaEnumDto(
string EncodingType,
string SemanticType,
List Choices
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaFieldDto.cs b/src/SbeCodeGenerator/Schema/SchemaFieldDto.cs
index 0fd58a0..3e23174 100644
--- a/src/SbeCodeGenerator/Schema/SchemaFieldDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaFieldDto.cs
@@ -22,5 +22,8 @@ internal record SchemaFieldDto(
string Deprecated,
string CharacterEncoding = "",
string SemanticType = ""
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaGroupDto.cs b/src/SbeCodeGenerator/Schema/SchemaGroupDto.cs
index f271acd..43a386f 100644
--- a/src/SbeCodeGenerator/Schema/SchemaGroupDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaGroupDto.cs
@@ -14,5 +14,8 @@ internal record SchemaGroupDto(
List Constants,
List? Data = null,
List? Groups = null
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaMessageDto.cs b/src/SbeCodeGenerator/Schema/SchemaMessageDto.cs
index e0a545c..ff5ff37 100644
--- a/src/SbeCodeGenerator/Schema/SchemaMessageDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaMessageDto.cs
@@ -16,5 +16,8 @@ internal record SchemaMessageDto(
List Groups,
List Data,
string BlockLength = ""
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaReader.cs b/src/SbeCodeGenerator/Schema/SchemaReader.cs
index 4821621..5263d65 100644
--- a/src/SbeCodeGenerator/Schema/SchemaReader.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaReader.cs
@@ -1,7 +1,10 @@
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
using SbeSourceGenerator.Diagnostics;
+using SbeSourceGenerator.Helpers;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Xml;
@@ -14,7 +17,7 @@ namespace SbeSourceGenerator.Schema
///
internal static class SchemaReader
{
- public static ParsedSchema Parse(string xmlContent, SourceProductionContext sourceContext)
+ public static ParsedSchema Parse(string xmlContent, SourceProductionContext sourceContext, string filePath = "", SourceText? sourceText = null)
{
var types = new List(16);
var composites = new List(8);
@@ -29,6 +32,7 @@ public static ParsedSchema Parse(string xmlContent, SourceProductionContext sour
string description = "";
string semanticVersion = "";
string headerType = "messageHeader";
+ var schemaSource = SchemaSourceInfo.Empty;
var settings = new XmlReaderSettings
{
@@ -48,6 +52,7 @@ public static ParsedSchema Parse(string xmlContent, SourceProductionContext sour
switch (reader.LocalName)
{
case "messageSchema":
+ schemaSource = CreateSourceInfo(reader, filePath, sourceText);
byteOrder = reader.GetAttribute("byteOrder") ?? "";
package = reader.GetAttribute("package") ?? "";
version = reader.GetAttribute("version") ?? "";
@@ -58,18 +63,21 @@ public static ParsedSchema Parse(string xmlContent, SourceProductionContext sour
break;
case "types":
- ReadTypes(reader, types, composites, enums, sets, sourceContext);
+ ReadTypes(reader, types, composites, enums, sets, sourceContext, filePath, sourceText);
break;
case "message":
- messages.Add(ReadMessage(reader, sourceContext));
+ messages.Add(ReadMessage(reader, sourceContext, filePath, sourceText));
break;
}
}
}
return new ParsedSchema(byteOrder, package, version, id, description, semanticVersion,
- types, composites, enums, sets, messages, headerType);
+ types, composites, enums, sets, messages, headerType)
+ {
+ Source = schemaSource
+ };
}
public static ParsedSchema Parse(string xmlContent)
@@ -82,7 +90,9 @@ private static void ReadTypes(XmlReader reader,
List composites,
List enums,
List sets,
- SourceProductionContext sourceContext)
+ SourceProductionContext sourceContext,
+ string filePath,
+ SourceText? sourceText)
{
if (reader.IsEmptyElement)
return;
@@ -99,26 +109,27 @@ private static void ReadTypes(XmlReader reader,
switch (reader.LocalName)
{
case "type":
- types.Add(ReadType(reader, sourceContext));
+ types.Add(ReadType(reader, sourceContext, filePath, sourceText));
break;
case "composite":
- composites.Add(ReadComposite(reader, sourceContext));
+ composites.Add(ReadComposite(reader, sourceContext, filePath, sourceText));
break;
case "enum":
- enums.Add(ReadEnum(reader, sourceContext));
+ enums.Add(ReadEnum(reader, sourceContext, filePath, sourceText));
break;
case "set":
- sets.Add(ReadSet(reader, sourceContext));
+ sets.Add(ReadSet(reader, sourceContext, filePath, sourceText));
break;
}
}
}
- private static SchemaTypeDto ReadType(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaTypeDto ReadType(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", "type", sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", "type", sourceContext, source);
string desc = reader.GetAttribute("description") ?? "";
- string primitiveType = GetRequiredAttribute(reader, "primitiveType", "type", sourceContext);
+ string primitiveType = GetRequiredAttribute(reader, "primitiveType", "type", sourceContext, source);
string semanticType = reader.GetAttribute("semanticType") ?? "";
string presence = reader.GetAttribute("presence") ?? "";
string nullValue = reader.GetAttribute("nullValue") ?? "";
@@ -140,12 +151,16 @@ private static SchemaTypeDto ReadType(XmlReader reader, SourceProductionContext
}
}
- return new SchemaTypeDto(name, desc, primitiveType, semanticType, presence, nullValue, length, innerText, minValue, maxValue, characterEncoding);
+ return new SchemaTypeDto(name, desc, primitiveType, semanticType, presence, nullValue, length, innerText, minValue, maxValue, characterEncoding)
+ {
+ Source = source
+ };
}
- private static SchemaCompositeDto ReadComposite(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaCompositeDto ReadComposite(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", "composite", sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", "composite", sourceContext, source);
string desc = reader.GetAttribute("description") ?? "";
string semanticType = reader.GetAttribute("semanticType") ?? "";
@@ -165,14 +180,17 @@ private static SchemaCompositeDto ReadComposite(XmlReader reader, SourceProducti
switch (reader.LocalName)
{
case "composite":
- var nested = ReadComposite(reader, sourceContext);
+ var nested = ReadComposite(reader, sourceContext, filePath, sourceText);
nestedComposites.Add(nested);
// Add a ref-like field placeholder to preserve ordering
fields.Add(new SchemaFieldDto(nested.Name, nested.Description,
- "", "", "", "", "", "", "", "", nested.Name, "", "", "", ""));
+ "", "", "", "", "", "", "", "", nested.Name, "", "", "", "")
+ {
+ Source = nested.Source
+ });
break;
case "enum":
- var nestedEnum = ReadEnum(reader, sourceContext);
+ var nestedEnum = ReadEnum(reader, sourceContext, filePath, sourceText);
nestedEnums.Add(nestedEnum);
break;
case "set":
@@ -189,21 +207,25 @@ private static SchemaCompositeDto ReadComposite(XmlReader reader, SourceProducti
}
break;
default:
- fields.Add(ReadField(reader, sourceContext));
+ fields.Add(ReadField(reader, sourceContext, filePath, sourceText));
break;
}
}
}
}
- return new SchemaCompositeDto(name, desc, semanticType, fields, nestedComposites, nestedEnums);
+ return new SchemaCompositeDto(name, desc, semanticType, fields, nestedComposites, nestedEnums)
+ {
+ Source = source
+ };
}
- private static SchemaEnumDto ReadEnum(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaEnumDto ReadEnum(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", "enum", sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", "enum", sourceContext, source);
string desc = reader.GetAttribute("description") ?? "";
- string encodingType = GetRequiredAttribute(reader, "encodingType", "enum", sourceContext);
+ string encodingType = GetRequiredAttribute(reader, "encodingType", "enum", sourceContext, source);
string semanticType = reader.GetAttribute("semanticType") ?? "";
var choices = new List(16);
@@ -215,23 +237,27 @@ private static SchemaEnumDto ReadEnum(XmlReader reader, SourceProductionContext
if (reader.NodeType == XmlNodeType.EndElement && reader.Depth == depth)
break;
if (reader.NodeType == XmlNodeType.Element)
- choices.Add(ReadField(reader, sourceContext));
+ choices.Add(ReadField(reader, sourceContext, filePath, sourceText));
}
}
- return new SchemaEnumDto(name, desc, encodingType, semanticType, choices);
+ return new SchemaEnumDto(name, desc, encodingType, semanticType, choices)
+ {
+ Source = source
+ };
}
- private static SchemaEnumDto ReadSet(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaEnumDto ReadSet(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
// Sets use the same DTO as enums
- return ReadEnum(reader, sourceContext);
+ return ReadEnum(reader, sourceContext, filePath, sourceText);
}
- private static SchemaMessageDto ReadMessage(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaMessageDto ReadMessage(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", "message", sourceContext);
- string msgId = GetRequiredAttribute(reader, "id", "message", sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", "message", sourceContext, source);
+ string msgId = GetRequiredAttribute(reader, "id", "message", sourceContext, source);
string desc = reader.GetAttribute("description") ?? "";
string semanticType = reader.GetAttribute("semanticType") ?? "";
string deprecated = reader.GetAttribute("deprecated") ?? "";
@@ -255,29 +281,33 @@ private static SchemaMessageDto ReadMessage(XmlReader reader, SourceProductionCo
switch (reader.LocalName)
{
case "field":
- var field = ReadField(reader, sourceContext);
+ var field = ReadField(reader, sourceContext, filePath, sourceText);
if (field.Presence == "constant")
constants.Add(field);
else
fields.Add(field);
break;
case "group":
- groups.Add(ReadGroup(reader, sourceContext));
+ groups.Add(ReadGroup(reader, sourceContext, filePath, sourceText));
break;
case "data":
- data.Add(ReadData(reader, sourceContext));
+ data.Add(ReadData(reader, sourceContext, filePath, sourceText));
break;
}
}
}
- return new SchemaMessageDto(name, msgId, desc, semanticType, deprecated, fields, constants, groups, data, blockLengthAttr);
+ return new SchemaMessageDto(name, msgId, desc, semanticType, deprecated, fields, constants, groups, data, blockLengthAttr)
+ {
+ Source = source
+ };
}
- private static SchemaGroupDto ReadGroup(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaGroupDto ReadGroup(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", "group", sourceContext);
- string groupId = GetRequiredAttribute(reader, "id", "group", sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", "group", sourceContext, source);
+ string groupId = GetRequiredAttribute(reader, "id", "group", sourceContext, source);
string dimensionType = reader.GetAttribute("dimensionType") ?? "";
if (string.IsNullOrEmpty(dimensionType))
dimensionType = "GroupSizeEncoding";
@@ -300,7 +330,7 @@ private static SchemaGroupDto ReadGroup(XmlReader reader, SourceProductionContex
if (reader.LocalName == "field")
{
- var field = ReadField(reader, sourceContext);
+ var field = ReadField(reader, sourceContext, filePath, sourceText);
if (field.Presence == "constant")
constants.Add(field);
else
@@ -308,37 +338,45 @@ private static SchemaGroupDto ReadGroup(XmlReader reader, SourceProductionContex
}
else if (reader.LocalName == "data")
{
- dataList.Add(ReadData(reader, sourceContext));
+ dataList.Add(ReadData(reader, sourceContext, filePath, sourceText));
}
else if (reader.LocalName == "group")
{
- nestedGroups.Add(ReadGroup(reader, sourceContext));
+ nestedGroups.Add(ReadGroup(reader, sourceContext, filePath, sourceText));
}
}
}
return new SchemaGroupDto(name, groupId, dimensionType, desc, fields, constants,
dataList.Count > 0 ? dataList : null,
- nestedGroups.Count > 0 ? nestedGroups : null);
+ nestedGroups.Count > 0 ? nestedGroups : null)
+ {
+ Source = source
+ };
}
- private static SchemaDataDto ReadData(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaDataDto ReadData(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", "data", sourceContext);
- string dataId = GetRequiredAttribute(reader, "id", "data", sourceContext);
- string type = GetRequiredAttribute(reader, "type", "data", sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", "data", sourceContext, source);
+ string dataId = GetRequiredAttribute(reader, "id", "data", sourceContext, source);
+ string type = GetRequiredAttribute(reader, "type", "data", sourceContext, source);
string desc = reader.GetAttribute("description") ?? "";
string sinceVersion = reader.GetAttribute("sinceVersion") ?? "";
if (!reader.IsEmptyElement)
reader.Skip();
- return new SchemaDataDto(name, dataId, type, desc, sinceVersion);
+ return new SchemaDataDto(name, dataId, type, desc, sinceVersion)
+ {
+ Source = source
+ };
}
- private static SchemaFieldDto ReadField(XmlReader reader, SourceProductionContext sourceContext)
+ private static SchemaFieldDto ReadField(XmlReader reader, SourceProductionContext sourceContext, string filePath, SourceText? sourceText)
{
- string name = GetRequiredAttribute(reader, "name", reader.LocalName, sourceContext);
+ var source = CreateSourceInfo(reader, filePath, sourceText);
+ string name = GetRequiredAttribute(reader, "name", reader.LocalName, sourceContext, source);
string desc = reader.GetAttribute("description") ?? "";
string primitiveType = reader.GetAttribute("primitiveType") ?? "";
string presence = reader.GetAttribute("presence") ?? "";
@@ -371,24 +409,57 @@ private static SchemaFieldDto ReadField(XmlReader reader, SourceProductionContex
}
return new SchemaFieldDto(name, desc, primitiveType, presence, length, nullValue, valueRef,
- innerText, fieldId, offset, type, sinceVersion, minValue, maxValue, deprecated, characterEncoding, semanticType);
+ innerText, fieldId, offset, type, sinceVersion, minValue, maxValue, deprecated, characterEncoding, semanticType)
+ {
+ Source = source
+ };
}
- private static string GetRequiredAttribute(XmlReader reader, string attributeName, string elementName, SourceProductionContext sourceContext)
+ private static string GetRequiredAttribute(XmlReader reader, string attributeName, string elementName, SourceProductionContext sourceContext, SchemaSourceInfo source)
{
string value = reader.GetAttribute(attributeName) ?? "";
if (string.IsNullOrEmpty(value))
{
- if (sourceContext.CancellationToken != default)
+ if (sourceContext.CanReportDiagnostics())
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.MissingRequiredAttribute,
- Location.None,
+ source.GetAttributeOrElement(attributeName),
attributeName,
elementName));
}
}
return value;
}
+
+ private static SchemaSourceInfo CreateSourceInfo(XmlReader reader, string filePath, SourceText? sourceText)
+ {
+ if (sourceText == null || string.IsNullOrWhiteSpace(filePath))
+ return SchemaSourceInfo.Empty;
+
+ var elementLocation = XmlDiagnosticLocation.CreateFromLineInfo(
+ sourceText,
+ filePath,
+ (IXmlLineInfo)reader,
+ reader.Name.Length + 1);
+
+ var attributeLocations = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal);
+ if (reader.HasAttributes && reader.MoveToFirstAttribute())
+ {
+ do
+ {
+ attributeLocations[reader.LocalName] = XmlDiagnosticLocation.CreateFromLineInfo(
+ sourceText,
+ filePath,
+ (IXmlLineInfo)reader,
+ reader.Name.Length);
+ }
+ while (reader.MoveToNextAttribute());
+
+ reader.MoveToElement();
+ }
+
+ return new SchemaSourceInfo(elementLocation, attributeLocations.ToImmutable());
+ }
}
}
diff --git a/src/SbeCodeGenerator/Schema/SchemaSourceInfo.cs b/src/SbeCodeGenerator/Schema/SchemaSourceInfo.cs
new file mode 100644
index 0000000..45d4e9f
--- /dev/null
+++ b/src/SbeCodeGenerator/Schema/SchemaSourceInfo.cs
@@ -0,0 +1,36 @@
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+
+namespace SbeSourceGenerator.Schema
+{
+ ///
+ /// Carries XML source locations for a parsed schema node and its attributes.
+ ///
+ internal sealed class SchemaSourceInfo
+ {
+ public static SchemaSourceInfo Empty { get; } =
+ new SchemaSourceInfo(Location.None, ImmutableDictionary.Empty);
+
+ public SchemaSourceInfo(Location elementLocation, ImmutableDictionary attributeLocations)
+ {
+ ElementLocation = elementLocation ?? Location.None;
+ AttributeLocations = attributeLocations ?? ImmutableDictionary.Empty;
+ }
+
+ public Location ElementLocation { get; }
+
+ public ImmutableDictionary AttributeLocations { get; }
+
+ public Location GetAttributeOrElement(string attributeName)
+ {
+ if (!string.IsNullOrEmpty(attributeName)
+ && AttributeLocations.TryGetValue(attributeName, out var location)
+ && location != Location.None)
+ {
+ return location;
+ }
+
+ return ElementLocation;
+ }
+ }
+}
diff --git a/src/SbeCodeGenerator/Schema/SchemaTypeDto.cs b/src/SbeCodeGenerator/Schema/SchemaTypeDto.cs
index 5586621..be971f3 100644
--- a/src/SbeCodeGenerator/Schema/SchemaTypeDto.cs
+++ b/src/SbeCodeGenerator/Schema/SchemaTypeDto.cs
@@ -15,5 +15,8 @@ internal record SchemaTypeDto(
string MinValue,
string MaxValue,
string CharacterEncoding = ""
- );
+ )
+ {
+ public SchemaSourceInfo Source { get; init; } = SchemaSourceInfo.Empty;
+ }
}
diff --git a/tests/SbeCodeGenerator.Tests/DiagnosticLocationTests.cs b/tests/SbeCodeGenerator.Tests/DiagnosticLocationTests.cs
new file mode 100644
index 0000000..6441aee
--- /dev/null
+++ b/tests/SbeCodeGenerator.Tests/DiagnosticLocationTests.cs
@@ -0,0 +1,114 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+using SbeSourceGenerator;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace SbeCodeGenerator.Tests
+{
+ public class DiagnosticLocationTests
+ {
+ 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 RunGenerator(string path, string content)
+ {
+ var compilation = CSharpCompilation.Create(
+ "DiagnosticLocationTests",
+ references: new[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ });
+
+ var driver = CSharpGeneratorDriver.Create(
+ generators: new[] { new SBESourceGenerator().AsSourceGenerator() },
+ additionalTexts: new AdditionalText[] { new InMemoryAdditionalText(path, content) }.ToImmutableArray());
+
+ var result = driver.RunGenerators(compilation).GetRunResult();
+ return result.Diagnostics
+ .AddRange(result.Results.SelectMany(r => r.Diagnostics));
+ }
+
+ [Fact]
+ public void Generator_MissingRequiredAttribute_UsesSchemaLocation()
+ {
+ var schema = @"
+
+
+
+
+
+
+
+
+
+
+
+
+";
+
+ var diagnostic = RunGenerator("/workspace/missing-name.xml", schema).First(d => d.Id == "SBE002");
+
+ Assert.NotEqual(Location.None, diagnostic.Location);
+ Assert.Equal("/workspace/missing-name.xml", diagnostic.Location.GetLineSpan().Path);
+ Assert.Equal(10, diagnostic.Location.GetLineSpan().StartLinePosition.Line);
+ }
+
+ [Fact]
+ public void Generator_InvalidIntegerAttribute_UsesAttributeLocation()
+ {
+ var schema = @"
+
+
+
+
+";
+
+ var diagnostic = RunGenerator("/workspace/invalid-length.xml", schema).First(d => d.Id == "SBE001");
+
+ Assert.NotEqual(Location.None, diagnostic.Location);
+ Assert.Equal("/workspace/invalid-length.xml", diagnostic.Location.GetLineSpan().Path);
+ Assert.Equal(3, diagnostic.Location.GetLineSpan().StartLinePosition.Line);
+ }
+
+ [Fact]
+ public void Generator_SinceVersionExceedsSchemaVersion_UsesAttributeLocation()
+ {
+ var schema = @"
+
+
+
+
+
+
+
+
+
+
+
+
+";
+
+ var diagnostic = RunGenerator("/workspace/since-version.xml", schema).First(d => d.Id == "SBE014");
+
+ Assert.NotEqual(Location.None, diagnostic.Location);
+ Assert.Equal("/workspace/since-version.xml", diagnostic.Location.GetLineSpan().Path);
+ Assert.Equal(11, diagnostic.Location.GetLineSpan().StartLinePosition.Line);
+ }
+ }
+}