Skip to content

Commit a89debf

Browse files
authored
Provide locations for src-gen diagnostic output (#61430)
* Provide locations for src-gen diagnostic output * Fix tests and address feedback * Add defensive check for context type location
1 parent 4d82463 commit a89debf

File tree

11 files changed

+232
-120
lines changed

11 files changed

+232
-120
lines changed

src/libraries/System.Text.Json/gen/ContextGenerationSpec.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text.Json.Serialization;
66
using System.Text.Json.Reflection;
77
using System.Diagnostics;
8+
using Microsoft.CodeAnalysis;
89

910
namespace System.Text.Json.SourceGeneration
1011
{
@@ -15,6 +16,8 @@ namespace System.Text.Json.SourceGeneration
1516
[DebuggerDisplay("ContextTypeRef={ContextTypeRef}")]
1617
internal sealed class ContextGenerationSpec
1718
{
19+
public Location Location { get; init; }
20+
1821
public JsonSourceGenerationOptionsAttribute GenerationOptions { get; init; }
1922

2023
public Type ContextType { get; init; }

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,9 @@ private void GenerateTypeInfo(TypeGenerationSpec typeGenerationSpec)
274274
break;
275275
case ClassType.TypeUnsupportedBySourceGen:
276276
{
277+
Location location = typeGenerationSpec.Type.GetDiagnosticLocation() ?? typeGenerationSpec.AttributeLocation ?? _currentContext.Location;
277278
_sourceGenerationContext.ReportDiagnostic(
278-
Diagnostic.Create(TypeNotSupported, Location.None, new string[] { typeGenerationSpec.TypeRef }));
279+
Diagnostic.Create(TypeNotSupported, location, new string[] { typeGenerationSpec.TypeRef }));
279280
return;
280281
}
281282
default:
@@ -293,7 +294,8 @@ private void GenerateTypeInfo(TypeGenerationSpec typeGenerationSpec)
293294
}
294295
else
295296
{
296-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(DuplicateTypeName, Location.None, new string[] { typeGenerationSpec.TypeInfoPropertyName }));
297+
Location location = typeGenerationSpec.AttributeLocation ?? _currentContext.Location;
298+
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(DuplicateTypeName, location, new string[] { typeGenerationSpec.TypeInfoPropertyName }));
297299
}
298300
}
299301

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ private sealed class Parser
116116

117117
private readonly HashSet<TypeGenerationSpec> _implicitlyRegisteredTypes = new();
118118

119+
/// <summary>
120+
/// A list of diagnostics emitted at the type level. This is cached and emitted between the processing of context types
121+
/// in order to properly retrieve [JsonSerializable] attribute applications for top-level types (since a type might occur
122+
/// both at top-level and in nested object graphs, with no guarantee of the order that we will see the type).
123+
/// The element tuple types specifies the serializable type, the kind of diagnostic to emit, and the diagnostic message.
124+
/// </summary>
125+
private readonly List<(Type, DiagnosticDescriptor, string[])> _typeLevelDiagnostics = new();
126+
119127
private JsonKnownNamingPolicy _currentContextNamingPolicy;
120128

121129
private static DiagnosticDescriptor ContextClassesMustBePartial { get; } = new DiagnosticDescriptor(
@@ -244,6 +252,11 @@ public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGene
244252

245253
foreach (ClassDeclarationSyntax classDeclarationSyntax in classDeclarationSyntaxList)
246254
{
255+
// Ensure context-scoped metadata caches are empty.
256+
Debug.Assert(_typeGenerationSpecCache.Count == 0);
257+
Debug.Assert(_implicitlyRegisteredTypes.Count == 0);
258+
Debug.Assert(_typeLevelDiagnostics.Count == 0);
259+
247260
CompilationUnitSyntax compilationUnitSyntax = classDeclarationSyntax.FirstAncestorOrSelf<CompilationUnitSyntax>();
248261
SemanticModel compilationSemanticModel = compilation.GetSemanticModel(compilationUnitSyntax.SyntaxTree);
249262

@@ -285,15 +298,18 @@ public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGene
285298
INamedTypeSymbol contextTypeSymbol = (INamedTypeSymbol)compilationSemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
286299
Debug.Assert(contextTypeSymbol != null);
287300

301+
Location contextLocation = contextTypeSymbol.Locations.Length > 0 ? contextTypeSymbol.Locations[0] : Location.None;
302+
288303
if (!TryGetClassDeclarationList(contextTypeSymbol, out List<string> classDeclarationList))
289304
{
290305
// Class or one of its containing types is not partial so we can't add to it.
291-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(ContextClassesMustBePartial, Location.None, new string[] { contextTypeSymbol.Name }));
306+
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(ContextClassesMustBePartial, contextLocation, new string[] { contextTypeSymbol.Name }));
292307
continue;
293308
}
294309

295310
ContextGenerationSpec contextGenSpec = new()
296311
{
312+
Location = contextLocation,
297313
GenerationOptions = options ?? new JsonSourceGenerationOptionsAttribute(),
298314
ContextType = contextTypeSymbol.AsType(_metadataLoadContext),
299315
ContextClassDeclarationList = classDeclarationList
@@ -316,14 +332,31 @@ public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGene
316332
continue;
317333
}
318334

335+
// Emit type-level diagnostics
336+
foreach ((Type Type, DiagnosticDescriptor Descriptor, string[] MessageArgs) diagnostic in _typeLevelDiagnostics)
337+
{
338+
Type type = diagnostic.Type;
339+
Location location = type.GetDiagnosticLocation();
340+
341+
if (location == null)
342+
{
343+
TypeGenerationSpec spec = _typeGenerationSpecCache[type];
344+
location = spec.AttributeLocation;
345+
}
346+
347+
location ??= contextLocation;
348+
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(diagnostic.Descriptor, location, diagnostic.MessageArgs));
349+
}
350+
319351
contextGenSpec.ImplicitlyRegisteredTypes.UnionWith(_implicitlyRegisteredTypes);
320352

321353
contextGenSpecList ??= new List<ContextGenerationSpec>();
322354
contextGenSpecList.Add(contextGenSpec);
323355

324-
// Clear the cache of generated metadata between the processing of context classes.
356+
// Clear the caches of generated metadata between the processing of context classes.
325357
_typeGenerationSpecCache.Clear();
326358
_implicitlyRegisteredTypes.Clear();
359+
_typeLevelDiagnostics.Clear();
327360
}
328361

329362
if (contextGenSpecList == null)
@@ -487,6 +520,8 @@ private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [Not
487520
typeGenerationSpec.GenerationMode = generationMode;
488521
}
489522

523+
typeGenerationSpec.AttributeLocation = attributeSyntax.GetLocation();
524+
490525
return typeGenerationSpec;
491526
}
492527

@@ -877,7 +912,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
877912
if (!type.TryGetDeserializationConstructor(useDefaultCtorInAnnotatedStructs, out ConstructorInfo? constructor))
878913
{
879914
classType = ClassType.TypeUnsupportedBySourceGen;
880-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(MultipleJsonConstructorAttribute, Location.None, new string[] { $"{type}" }));
915+
_typeLevelDiagnostics.Add((type, MultipleJsonConstructorAttribute, new string[] { $"{type}" }));
881916
}
882917
else
883918
{
@@ -944,7 +979,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
944979
}
945980

946981
spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode);
947-
CacheMemberHelper();
982+
CacheMemberHelper(propertyInfo.GetDiagnosticLocation());
948983
}
949984

950985
foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags))
@@ -955,10 +990,10 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
955990
}
956991

957992
spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode);
958-
CacheMemberHelper();
993+
CacheMemberHelper(fieldInfo.GetDiagnosticLocation());
959994
}
960995

961-
void CacheMemberHelper()
996+
void CacheMemberHelper(Location memberLocation)
962997
{
963998
CacheMember(spec, ref propGenSpecList, ref ignoredMembers);
964999

@@ -972,13 +1007,13 @@ void CacheMemberHelper()
9721007
{
9731008
if (dataExtensionPropGenSpec != null)
9741009
{
975-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(MultipleJsonExtensionDataAttribute, Location.None, new string[] { type.Name }));
1010+
_typeLevelDiagnostics.Add((type, MultipleJsonExtensionDataAttribute, new string[] { type.Name }));
9761011
}
9771012

9781013
Type propType = spec.TypeGenerationSpec.Type;
9791014
if (!IsValidDataExtensionPropertyType(propType))
9801015
{
981-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(DataExtensionPropertyInvalid, Location.None, new string[] { type.Name, spec.ClrName }));
1016+
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(DataExtensionPropertyInvalid, memberLocation, new string[] { type.Name, spec.ClrName }));
9821017
}
9831018

9841019
dataExtensionPropGenSpec = GetOrAddTypeGenerationSpec(propType, generationMode);
@@ -987,13 +1022,13 @@ void CacheMemberHelper()
9871022

9881023
if (!hasInitOnlyProperties && spec.CanUseSetter && spec.IsInitOnlySetter)
9891024
{
990-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InitOnlyPropertyDeserializationNotSupported, Location.None, new string[] { type.Name }));
1025+
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InitOnlyPropertyDeserializationNotSupported, memberLocation, new string[] { type.Name }));
9911026
hasInitOnlyProperties = true;
9921027
}
9931028

9941029
if (spec.HasJsonInclude && (!spec.CanUseGetter || !spec.CanUseSetter || !spec.IsPublic))
9951030
{
996-
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InaccessibleJsonIncludePropertiesNotSupported, Location.None, new string[] { type.Name, spec.ClrName }));
1031+
_sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InaccessibleJsonIncludePropertiesNotSupported, memberLocation, new string[] { type.Name, spec.ClrName }));
9971032
}
9981033
}
9991034
}

src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,7 @@ public override void SetValue(object obj, object value, BindingFlags invokeAttr,
100100
{
101101
throw new NotImplementedException();
102102
}
103+
104+
public Location? Location => _field.Locations.Length > 0 ? _field.Locations[0] : null;
103105
}
104106
}

src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,7 @@ public override void SetValue(object obj, object value, BindingFlags invokeAttr,
9292
{
9393
throw new NotSupportedException();
9494
}
95+
96+
public Location? Location => _property.Locations.Length > 0 ? _property.Locations[0] : null;
9597
}
9698
}

src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Linq;
77
using System.Reflection;
8+
using Microsoft.CodeAnalysis;
89

910
namespace System.Text.Json.Reflection
1011
{
@@ -45,5 +46,23 @@ private static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo)
4546

4647
return false;
4748
}
49+
50+
public static Location? GetDiagnosticLocation(this Type type)
51+
{
52+
Debug.Assert(type is TypeWrapper);
53+
return ((TypeWrapper)type).Location;
54+
}
55+
56+
public static Location? GetDiagnosticLocation(this PropertyInfo propertyInfo)
57+
{
58+
Debug.Assert(propertyInfo is PropertyInfoWrapper);
59+
return ((PropertyInfoWrapper)propertyInfo).Location;
60+
}
61+
62+
public static Location? GetDiagnosticLocation(this FieldInfo fieldInfo)
63+
{
64+
Debug.Assert(fieldInfo is FieldInfoWrapper);
65+
return ((FieldInfoWrapper)fieldInfo).Location;
66+
}
4867
}
4968
}

src/libraries/System.Text.Json/gen/Reflection/TypeWrapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,5 +600,7 @@ public override bool Equals(Type o)
600600
}
601601
return base.Equals(o);
602602
}
603+
604+
public Location? Location => _typeSymbol.Locations.Length > 0 ? _typeSymbol.Locations[0] : null;
603605
}
604606
}

src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Reflection;
88
using System.Text.Json.Reflection;
99
using System.Text.Json.Serialization;
10+
using Microsoft.CodeAnalysis;
1011

1112
namespace System.Text.Json.SourceGeneration
1213
{
@@ -18,6 +19,11 @@ internal class TypeGenerationSpec
1819
/// </summary>
1920
public string TypeRef { get; private set; }
2021

22+
/// <summary>
23+
/// If specified as a root type via <c>JsonSerializableAttribute</c>, specifies the location of the attribute application.
24+
/// </summary>
25+
public Location? AttributeLocation { get; set; }
26+
2127
/// <summary>
2228
/// The name of the public <c>JsonTypeInfo&lt;T&gt;</c> property for this type on the generated context class.
2329
/// For example, if the context class is named MyJsonContext, and the value of this property is JsonMessage;

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -336,22 +336,32 @@ public partial class MyJsonContext : JsonSerializerContext
336336
return CreateCompilation(source);
337337
}
338338

339-
internal static void CheckDiagnosticMessages(ImmutableArray<Diagnostic> diagnostics, DiagnosticSeverity level, string[] expectedMessages)
339+
internal static void CheckDiagnosticMessages(
340+
DiagnosticSeverity level,
341+
ImmutableArray<Diagnostic> diagnostics,
342+
(Location Location, string Message)[] expectedDiags,
343+
bool sort = true)
340344
{
341-
string[] actualMessages = diagnostics.Where(diagnostic => diagnostic.Severity == level).Select(diagnostic => diagnostic.GetMessage()).ToArray();
345+
(Location Location, string Message)[] actualDiags = diagnostics
346+
.Where(diagnostic => diagnostic.Severity == level)
347+
.Select(diagnostic => (diagnostic.Location, diagnostic.GetMessage()))
348+
.ToArray();
342349

343-
// Can't depend on reflection order when generating type metadata.
344-
Array.Sort(actualMessages);
345-
Array.Sort(expectedMessages);
350+
if (sort)
351+
{
352+
// Can't depend on reflection order when generating type metadata.
353+
Array.Sort(actualDiags);
354+
Array.Sort(expectedDiags);
355+
}
346356

347357
if (CultureInfo.CurrentUICulture.Name.StartsWith("en", StringComparison.OrdinalIgnoreCase))
348358
{
349-
Assert.Equal(expectedMessages, actualMessages);
359+
Assert.Equal(expectedDiags, actualDiags);
350360
}
351361
else
352362
{
353363
// for non-English runs, just compare the number of messages are the same
354-
Assert.Equal(expectedMessages.Length, actualMessages.Length);
364+
Assert.Equal(expectedDiags.Length, actualDiags.Length);
355365
}
356366
}
357367
}

0 commit comments

Comments
 (0)