Skip to content

Commit 4a3a17d

Browse files
Copilotstephentoub
andauthored
Fix source generator diagnostic locations for incremental caching (#1153)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent fcf70c7 commit 4a3a17d

File tree

2 files changed

+67
-22
lines changed

2 files changed

+67
-22
lines changed

src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private static Diagnostic CreateDiagnostic(DiagnosticInfo info) =>
7272
"MCP001" => Diagnostics.InvalidXmlDocumentation,
7373
"MCP002" => Diagnostics.McpMethodMustBePartial,
7474
_ => throw new InvalidOperationException($"Unknown diagnostic ID: {info.Id}")
75-
}, info.Location, info.MessageArgs);
75+
}, info.Location?.ToLocation(), info.MessageArgs);
7676

7777
private static IncrementalValuesProvider<MethodToGenerate> CreateProviderForAttribute(
7878
IncrementalGeneratorInitializationContext context,
@@ -108,15 +108,15 @@ private static MethodToGenerate ExtractMethodInfo(
108108
HasGeneratableContent(xmlDocs, methodSymbol, descriptionAttribute))
109109
{
110110
return MethodToGenerate.CreateDiagnosticOnly(
111-
new DiagnosticInfo("MCP002", methodDeclaration.Identifier.GetLocation(), methodSymbol.Name));
111+
DiagnosticInfo.Create("MCP002", methodDeclaration.Identifier.GetLocation(), methodSymbol.Name));
112112
}
113113

114114
return MethodToGenerate.Empty;
115115
}
116116

117117
// For partial methods with invalid XML, report diagnostic but still generate partial declaration.
118118
EquatableArray<DiagnosticInfo> diagnostics = hasInvalidXml ?
119-
new EquatableArray<DiagnosticInfo>(ImmutableArray.Create(new DiagnosticInfo("MCP001", methodSymbol.Locations.FirstOrDefault(), methodSymbol.Name))) :
119+
new EquatableArray<DiagnosticInfo>(ImmutableArray.Create(DiagnosticInfo.Create("MCP001", methodSymbol.Locations.FirstOrDefault(), methodSymbol.Name))) :
120120
default;
121121

122122
bool needsMethodDescription = xmlDocs is not null &&
@@ -514,29 +514,29 @@ private readonly record struct TypeDeclarationInfo(
514514
string Name,
515515
string TypeKeyword);
516516

517-
/// <summary>Holds diagnostic information to be reported.</summary>
518-
private readonly struct DiagnosticInfo
517+
/// <summary>Holds serializable location information for incremental generator caching.</summary>
518+
/// <remarks>
519+
/// Roslyn <see cref="Location"/> objects cannot be stored in incremental generator cached data
520+
/// because they contain references to syntax trees from specific compilations. Storing them
521+
/// causes issues when the generator returns cached data with locations from earlier compilations.
522+
/// </remarks>
523+
private readonly record struct LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan)
519524
{
520-
public string Id { get; }
521-
public Location? Location { get; }
522-
public string MethodName { get; }
523-
524-
public DiagnosticInfo(string id, Location? location, string methodName)
525-
{
526-
Id = id;
527-
Location = location;
528-
MethodName = methodName;
529-
}
530-
531-
public object?[] MessageArgs => [MethodName];
525+
public static LocationInfo? FromLocation(Location? location) =>
526+
location is null || !location.IsInSource ? null :
527+
new LocationInfo(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
532528

533-
// For incremental generator caching, we compare only the logical content, not the Location object
534-
public bool Equals(DiagnosticInfo other) =>
535-
Id == other.Id && MethodName == other.MethodName;
529+
public Location ToLocation() =>
530+
Location.Create(FilePath, TextSpan, LineSpan);
531+
}
536532

537-
public override bool Equals(object? obj) => obj is DiagnosticInfo other && Equals(other);
533+
/// <summary>Holds diagnostic information to be reported.</summary>
534+
private readonly record struct DiagnosticInfo(string Id, LocationInfo? Location, string MethodName)
535+
{
536+
public static DiagnosticInfo Create(string id, Location? location, string methodName) =>
537+
new(id, LocationInfo.FromLocation(location), methodName);
538538

539-
public override int GetHashCode() => (Id, MethodName).GetHashCode();
539+
public object?[] MessageArgs => [MethodName];
540540
}
541541

542542
/// <summary>Holds extracted XML documentation for a method (used only during extraction, not cached).</summary>

tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,51 @@ partial class TestTools
729729
Assert.Contains("invalid", diagnostic.GetMessage(), StringComparison.OrdinalIgnoreCase);
730730
}
731731

732+
[Fact]
733+
public void Generator_DiagnosticHasValidSourceLocation()
734+
{
735+
// This test verifies that diagnostic locations are properly reconstructed
736+
// and point to valid source positions (regression test for locations from stale compilations)
737+
var result = RunGenerator("""
738+
using ModelContextProtocol.Server;
739+
using System.ComponentModel;
740+
741+
namespace Test;
742+
743+
[McpServerToolType]
744+
public class TestTools
745+
{
746+
/// <summary>
747+
/// Test tool
748+
/// </summary>
749+
[McpServerTool]
750+
public static string TestMethod(string input)
751+
{
752+
return input;
753+
}
754+
}
755+
""", "MCP002");
756+
757+
Assert.True(result.Success);
758+
759+
// Verify the diagnostic has a valid location with correct line/column information
760+
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
761+
Assert.NotNull(diagnostic.Location);
762+
763+
// Verify the location span has valid position information
764+
var lineSpan = diagnostic.Location.GetLineSpan();
765+
Assert.True(lineSpan.IsValid, "Diagnostic line span should be valid");
766+
767+
// Verify reasonable location values without assuming specific line numbers
768+
Assert.True(lineSpan.StartLinePosition.Line >= 0, "Start line should be non-negative");
769+
Assert.True(lineSpan.StartLinePosition.Character >= 0, "Start character should be non-negative");
770+
Assert.True(lineSpan.EndLinePosition.Line >= lineSpan.StartLinePosition.Line, "End line should be >= start line");
771+
772+
// The span should have a non-zero length (the identifier "TestMethod" is 10 characters)
773+
Assert.True(diagnostic.Location.SourceSpan.Length > 0, "Span should have non-zero length");
774+
Assert.Equal(10, diagnostic.Location.SourceSpan.Length); // "TestMethod".Length == 10
775+
}
776+
732777
[Fact]
733778
public void Generator_WithGenericType_GeneratesCorrectly()
734779
{

0 commit comments

Comments
 (0)