Skip to content

Commit 49b57f5

Browse files
committed
Fix case where missing converter defaults
1 parent a2271ce commit 49b57f5

File tree

4 files changed

+289
-10
lines changed

4 files changed

+289
-10
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace StronglyTypedIds.Diagnostics;
4+
5+
internal static class MissingDefaultsDiagnostic
6+
{
7+
internal const string Id = "STRONGID005";
8+
internal const string Message = "You must specify the default template to use for converters using [StronglyTypedIdConvertersDefaults]";
9+
internal const string Title = "Missing [StronglyTypedIdConvertersDefaults] attribute";
10+
11+
public static DiagnosticInfo CreateInfo(Location location) =>
12+
new(new DiagnosticDescriptor(
13+
Id, Title, Message, category: Constants.Usage, defaultSeverity: DiagnosticSeverity.Warning,
14+
isEnabledByDefault: true),
15+
location);
16+
}

src/StronglyTypedIds/StronglyTypedIdGenerator.cs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5656
.Select((result, _) => result.Value.defaults)
5757
.Collect()
5858
.Combine(templates)
59-
.Select(ProcessDefaults);
59+
.Select(ProcessIdDefaults);
6060

6161
context.RegisterSourceOutput(
6262
idDefaultsAndDiagnostics.SelectMany((x, _) => x.Errors),
@@ -91,7 +91,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
9191
.Select((result, _) => result.Value.defaults)
9292
.Collect()
9393
.Combine(templates)
94-
.Select(ProcessDefaults);
94+
.Select(ProcessConverterDefaults);
9595

9696
context.RegisterSourceOutput(
9797
converterDefaultsAndDiagnostics.SelectMany((x, _) => x.Errors),
@@ -177,7 +177,7 @@ private static void GenerateIds(
177177
private static void GenerateConverters(
178178
ConverterToGenerate converterToGenerate,
179179
ImmutableArray<(string Path, string Name, string? Content)> templates,
180-
(EquatableArray<(string Name, string Content)>, bool IsValid, DiagnosticInfo? Diagnostic) defaults,
180+
(EquatableArray<(string Name, string Content)> Templates, bool IsValid, DiagnosticInfo? Diagnostic) defaults,
181181
SourceProductionContext context)
182182
{
183183
if (defaults.Diagnostic is { } diagnostic)
@@ -186,6 +186,19 @@ private static void GenerateConverters(
186186
context.ReportDiagnostic(diagnostic);
187187
}
188188

189+
if (converterToGenerate.TemplateNames.Count == 0
190+
&& defaults is { IsValid: false, Templates.Count: 0 })
191+
{
192+
// not allowed this, so add a diagnostic
193+
if (converterToGenerate.TemplateLocation is { } l)
194+
{
195+
var location = Location.Create(l.FilePath, l.TextSpan, l.LineSpan);
196+
context.ReportDiagnostic(MissingDefaultsDiagnostic.CreateInfo(location));
197+
}
198+
199+
return;
200+
}
201+
189202
if (!TryGetTemplateContent(selectedTemplate: null, converterToGenerate.TemplateNames, converterToGenerate.TemplateLocation, templates, defaults, in context, out var templateContents))
190203
{
191204
return;
@@ -220,22 +233,40 @@ private static void GenerateConverters(
220233
}
221234

222235

223-
private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessDefaults((ImmutableArray<Defaults> Left, ImmutableArray<(string Path, string Name, string? Content)> Right) all, CancellationToken _)
236+
private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessIdDefaults(
237+
(ImmutableArray<Defaults> Selected, ImmutableArray<(string Path, string Name, string? Content)> Templates) all,
238+
CancellationToken _)
239+
=> ProcessDefaults(all, allowEmptyDefaults: true);
240+
241+
private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessConverterDefaults(
242+
(ImmutableArray<Defaults> Selected, ImmutableArray<(string Path, string Name, string? Content)> Templates) all,
243+
CancellationToken _)
244+
=> ProcessDefaults(all, allowEmptyDefaults: false);
245+
246+
private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticInfo?) ProcessDefaults(
247+
(ImmutableArray<Defaults> Selected, ImmutableArray<(string Path, string Name, string? Content)> Templates) all,
248+
bool allowEmptyDefaults)
224249
{
225-
if (all.Left.IsDefaultOrEmpty)
250+
if (all.Selected.IsDefaultOrEmpty)
226251
{
227252
// no default attributes, valid, but no content
228-
return (EquatableArray<(string Name, string Content)>.Empty, true, null);
253+
if (allowEmptyDefaults)
254+
{
255+
return (EquatableArray<(string Name, string Content)>.Empty, true, null);
256+
}
257+
258+
// Not allowed empty
259+
return (EquatableArray<(string Name, string Content)>.Empty, false, null);
229260
}
230261

231262
// technically we can never have more than one `Defaults` here
232263
// but check for it just in case
233-
if (all.Left is {IsDefaultOrEmpty: false, Length: > 1})
264+
if (all.Selected is {IsDefaultOrEmpty: false, Length: > 1})
234265
{
235266
return (EquatableArray<(string Name, string Content)>.Empty, false, null);
236267
}
237268

238-
var defaults = all.Left[0];
269+
var defaults = all.Selected[0];
239270
if (defaults.HasMultiple)
240271
{
241272
// not valid
@@ -264,7 +295,7 @@ private static (EquatableArray<(string Name, string Content)>, bool, DiagnosticI
264295
}
265296

266297
// We have already checked for null/empty template name and flagged it as an error
267-
if (!GetContent(templateNames, defaults.TemplateLocation!, builtInTemplate.HasValue, in all.Right, out var contents, out var diagnostic))
298+
if (!GetContent(templateNames, defaults.TemplateLocation!, builtInTemplate.HasValue, in all.Templates, out var contents, out var diagnostic))
268299
{
269300
return (EquatableArray<(string Name, string Content)>.Empty, false, diagnostic);
270301
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by the StronglyTypedId source generator
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
#pragma warning disable 1591 // publicly visible type or member must be documented
11+
12+
#nullable enable
13+
[global::System.ComponentModel.TypeConverter(typeof(MyIdTypeConverter))]
14+
[global::System.Text.Json.Serialization.JsonConverter(typeof(MyIdSystemTextJsonConverter))]
15+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedId", "1.0.0-beta6")]
16+
partial struct MyId :
17+
#if NET6_0_OR_GREATER
18+
global::System.ISpanFormattable,
19+
#endif
20+
#if NET7_0_OR_GREATER
21+
global::System.IParsable<MyId>, global::System.ISpanParsable<MyId>,
22+
#endif
23+
#if NET8_0_OR_GREATER
24+
global::System.IUtf8SpanFormattable,
25+
#endif
26+
global::System.IComparable<MyId>, global::System.IEquatable<MyId>, global::System.IFormattable
27+
{
28+
public global::System.Guid Value { get; }
29+
30+
public MyId(global::System.Guid value)
31+
{
32+
Value = value;
33+
}
34+
35+
public static MyId New() => new MyId(global::System.Guid.NewGuid());
36+
public static readonly MyId Empty = new MyId(global::System.Guid.Empty);
37+
38+
/// <inheritdoc cref="global::System.IEquatable{T}"/>
39+
public bool Equals(MyId other) => this.Value.Equals(other.Value);
40+
public override bool Equals(object? obj)
41+
{
42+
if (ReferenceEquals(null, obj)) return false;
43+
return obj is MyId other && Equals(other);
44+
}
45+
46+
public override int GetHashCode() => Value.GetHashCode();
47+
48+
public override string ToString() => Value.ToString();
49+
50+
public static bool operator ==(MyId a, MyId b) => a.Equals(b);
51+
public static bool operator !=(MyId a, MyId b) => !(a == b);
52+
public static bool operator > (MyId a, MyId b) => a.CompareTo(b) > 0;
53+
public static bool operator < (MyId a, MyId b) => a.CompareTo(b) < 0;
54+
public static bool operator >= (MyId a, MyId b) => a.CompareTo(b) >= 0;
55+
public static bool operator <= (MyId a, MyId b) => a.CompareTo(b) <= 0;
56+
57+
/// <inheritdoc cref="global::System.IComparable{TSelf}"/>
58+
public int CompareTo(MyId other) => Value.CompareTo(other.Value);
59+
60+
public partial class MyIdTypeConverter : global::System.ComponentModel.TypeConverter
61+
{
62+
public override bool CanConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type sourceType)
63+
{
64+
return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
65+
}
66+
67+
public override object? ConvertFrom(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object value)
68+
{
69+
return value switch
70+
{
71+
global::System.Guid guidValue => new MyId(guidValue),
72+
string stringValue when !string.IsNullOrEmpty(stringValue) && global::System.Guid.TryParse(stringValue, out var result) => new MyId(result),
73+
_ => base.ConvertFrom(context, culture, value),
74+
};
75+
}
76+
77+
public override bool CanConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Type? sourceType)
78+
{
79+
return sourceType == typeof(global::System.Guid) || sourceType == typeof(string) || base.CanConvertTo(context, sourceType);
80+
}
81+
82+
public override object? ConvertTo(global::System.ComponentModel.ITypeDescriptorContext? context, global::System.Globalization.CultureInfo? culture, object? value, global::System.Type destinationType)
83+
{
84+
if (value is MyId idValue)
85+
{
86+
if (destinationType == typeof(global::System.Guid))
87+
{
88+
return idValue.Value;
89+
}
90+
91+
if (destinationType == typeof(string))
92+
{
93+
return idValue.Value.ToString();
94+
}
95+
}
96+
97+
return base.ConvertTo(context, culture, value, destinationType);
98+
}
99+
}
100+
101+
public partial class MyIdSystemTextJsonConverter : global::System.Text.Json.Serialization.JsonConverter<MyId>
102+
{
103+
public override bool CanConvert(global::System.Type typeToConvert)
104+
=> typeToConvert == typeof(global::System.Guid) || typeToConvert == typeof(string) || base.CanConvert(typeToConvert);
105+
106+
public override MyId Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options)
107+
=> new (reader.GetGuid());
108+
109+
public override void Write(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options)
110+
=> writer.WriteStringValue(value.Value);
111+
112+
#if NET6_0_OR_GREATER
113+
public override MyId ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options)
114+
=> new(global::System.Guid.Parse(reader.GetString() ?? throw new global::System.FormatException("The string for the MyId property was null")));
115+
116+
public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, MyId value, global::System.Text.Json.JsonSerializerOptions options)
117+
=> writer.WritePropertyName(value.Value.ToString());
118+
#endif
119+
}
120+
121+
public static MyId Parse(string input)
122+
=> new(global::System.Guid.Parse(input));
123+
124+
#if NET7_0_OR_GREATER
125+
/// <inheritdoc cref="global::System.IParsable{TSelf}"/>
126+
public static MyId Parse(string input, global::System.IFormatProvider? provider)
127+
=> new(global::System.Guid.Parse(input, provider));
128+
129+
/// <inheritdoc cref="global::System.IParsable{TSelf}"/>
130+
public static bool TryParse(
131+
[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? input,
132+
global::System.IFormatProvider? provider,
133+
out MyId result)
134+
{
135+
if (input is null)
136+
{
137+
result = default;
138+
return false;
139+
}
140+
141+
if (global::System.Guid.TryParse(input, provider, out var guid))
142+
{
143+
result = new(guid);
144+
return true;
145+
}
146+
else
147+
{
148+
result = default;
149+
return false;
150+
}
151+
}
152+
#endif
153+
154+
/// <inheritdoc cref="global::System.IFormattable"/>
155+
public string ToString(
156+
#if NET7_0_OR_GREATER
157+
[global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)]
158+
#endif
159+
string? format,
160+
global::System.IFormatProvider? formatProvider)
161+
=> Value.ToString(format, formatProvider);
162+
163+
#if NETCOREAPP2_1_OR_GREATER
164+
public static MyId Parse(global::System.ReadOnlySpan<char> input)
165+
=> new(global::System.Guid.Parse(input));
166+
#endif
167+
168+
#if NET6_0_OR_GREATER
169+
#if NET7_0_OR_GREATER
170+
/// <inheritdoc cref="global::System.ISpanParsable{TSelf}"/>
171+
#endif
172+
public static MyId Parse(global::System.ReadOnlySpan<char> input, global::System.IFormatProvider? provider)
173+
#if NET7_0_OR_GREATER
174+
=> new(global::System.Guid.Parse(input, provider));
175+
#else
176+
=> new(global::System.Guid.Parse(input));
177+
#endif
178+
179+
#if NET7_0_OR_GREATER
180+
/// <inheritdoc cref="global::System.ISpanParsable{TSelf}"/>
181+
#endif
182+
public static bool TryParse(global::System.ReadOnlySpan<char> input, global::System.IFormatProvider? provider, out MyId result)
183+
{
184+
#if NET7_0_OR_GREATER
185+
if (global::System.Guid.TryParse(input, provider, out var guid))
186+
#else
187+
if (global::System.Guid.TryParse(input, out var guid))
188+
#endif
189+
{
190+
result = new(guid);
191+
return true;
192+
}
193+
else
194+
{
195+
result = default;
196+
return false;
197+
}
198+
}
199+
200+
/// <inheritdoc cref="global::System.ISpanFormattable"/>
201+
public bool TryFormat(
202+
global::System.Span<char> destination,
203+
out int charsWritten,
204+
#if NET7_0_OR_GREATER
205+
[global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)]
206+
#endif
207+
global::System.ReadOnlySpan<char> format,
208+
global::System.IFormatProvider? provider)
209+
=> Value.TryFormat(destination, out charsWritten, format);
210+
211+
/// <inheritdoc cref="global::System.ISpanFormattable"/>
212+
public bool TryFormat(
213+
global::System.Span<char> destination,
214+
out int charsWritten,
215+
#if NET7_0_OR_GREATER
216+
[global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)]
217+
#endif
218+
global::System.ReadOnlySpan<char> format = default)
219+
=> Value.TryFormat(destination, out charsWritten, format);
220+
#endif
221+
#if NET8_0_OR_GREATER
222+
/// <inheritdoc cref="global::System.IUtf8SpanFormattable.TryFormat" />
223+
public bool TryFormat(
224+
global::System.Span<byte> utf8Destination,
225+
out int bytesWritten,
226+
[global::System.Diagnostics.CodeAnalysis.StringSyntax(global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.GuidFormat)]
227+
global::System.ReadOnlySpan<char> format,
228+
global::System.IFormatProvider? provider)
229+
=> Value.TryFormat(utf8Destination, out bytesWritten, format);
230+
#endif
231+
}

test/StronglyTypedIds.Tests/StronglyTypedIdConverterTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Threading.Tasks;
2+
using StronglyTypedIds.Diagnostics;
23
using VerifyXunit;
34
using Xunit;
45
using Xunit.Abstractions;
@@ -30,7 +31,7 @@ public partial struct MyIdConverters {}
3031
""";
3132
var (diagnostics, output) = TestHelpers.GetGeneratedOutput<StronglyTypedIdGenerator>(input, includeAttributes: false);
3233

33-
Assert.Empty(diagnostics);
34+
Assert.Contains(diagnostics, diagnostic => diagnostic.Id == MissingDefaultsDiagnostic.Id);
3435

3536
return Verifier.Verify(output)
3637
.UseDirectory("Snapshots");

0 commit comments

Comments
 (0)