Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/SbeCodeGenerator/Diagnostics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 55 additions & 0 deletions src/SbeCodeGenerator/Diagnostics/XmlDiagnosticLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Xml;

namespace SbeSourceGenerator.Diagnostics
{
/// <summary>
/// Creates Roslyn <see cref="Location"/> instances for XML additional files.
/// </summary>
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);
}
}
}
55 changes: 33 additions & 22 deletions src/SbeCodeGenerator/Generators/MessagesCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -162,11 +167,11 @@ private static List<int> 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()));
Expand All @@ -177,11 +182,11 @@ private static List<int> 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));
Expand All @@ -195,11 +200,11 @@ private static List<int> 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()));
Expand All @@ -208,11 +213,11 @@ private static List<int> 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));
Expand Down Expand Up @@ -318,7 +323,7 @@ private static List<IFileContentGenerator> 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,
Expand Down Expand Up @@ -354,7 +359,7 @@ private static List<IFileContentGenerator> 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,
Expand All @@ -372,7 +377,7 @@ private static List<IFileContentGenerator> 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,
Expand Down Expand Up @@ -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),
Expand All @@ -454,7 +459,7 @@ private static void TryAppendSemanticAccessor(
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.SemanticAccessorNameCollision,
registration.Location ?? Location.None,
field.Source.GetAttributeOrElement("semanticType"),
generatedFieldName,
"<message>",
registration.SemanticType));
Expand Down Expand Up @@ -508,7 +513,11 @@ private static List<IFileContentGenerator> BuildGroups(
var result = new List<IFileContentGenerator>(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<IFileContentGenerator>(group.Fields.Count);
foreach (var field in group.Fields)
Expand All @@ -522,7 +531,7 @@ private static List<IFileContentGenerator> 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,
Expand All @@ -533,7 +542,9 @@ private static List<IFileContentGenerator> 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,
Expand Down Expand Up @@ -600,7 +611,7 @@ private static List<IFileContentGenerator> GetDataForVersion(List<SchemaDataDto>
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))
Expand All @@ -611,15 +622,15 @@ 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."));
}
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;
Expand All @@ -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));
Expand Down
17 changes: 13 additions & 4 deletions src/SbeCodeGenerator/Generators/TypeResolverHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -132,7 +141,7 @@ public static string RegisterGeneratedTypeName(SchemaContext context, string ori
{
sourceContext.ReportDiagnostic(Diagnostic.Create(
SbeDiagnostics.DuplicateTypeName,
Location.None,
location ?? Location.None,
originalName));
}
}
Expand Down
Loading
Loading