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); + } + } +}