• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ImmediatePlatform / Immediate.Jobs / 30564386813

30 Jul 2026 05:04PM UTC coverage: 80.665% (+1.5%) from 79.176%
30564386813

Pull #52

github

web-flow
Merge 8213c2de3 into d22b04225
Pull Request #52: Fix batch cancellation crash when members have dependency chains

6 of 6 new or added lines in 1 file covered. (100.0%)

25 existing lines in 3 files now uncovered.

7063 of 8756 relevant lines covered (80.66%)

2.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.85
/src/Immediate.Jobs.Generators/JsonMetadataEmitter.cs
1
using System.Collections.Immutable;
2
using Microsoft.CodeAnalysis;
3

4
namespace Immediate.Jobs.Generators;
5

6
internal static class JsonMetadataEmitter
7
{
8
        public static JsonMetadataRenderModel CreateModel(
9
                ITypeSymbol payloadType,
10
                IEnumerable<ITypeSymbol> contextTypes
11
        )
12
        {
13
                var types = CollectTypes([payloadType, .. contextTypes]);
4✔
14

15
                return new()
4✔
16
                {
4✔
17
                        PayloadTypeName = payloadType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
18
                        Types = types.Select(CreateTypeModel).ToEquatableReadOnlyList(),
4✔
19
                };
4✔
20
        }
21

22
        private static JsonTypeRenderModel CreateTypeModel(ITypeSymbol type, int index)
23
        {
24
                var converterName = GetConverter(type);
4✔
25
                var isEnum = type.TypeKind == TypeKind.Enum;
4✔
26
                var usesConfiguredConverter = type.RootNamespace == "NodaTime";
4✔
27
                var collectionInfo = converterName is null ? CreateCollectionModel(type) : null;
4✔
28

29
                JsonObjectRenderModel? objectInfo = null;
4✔
30
                if (converterName is null && !isEnum && !usesConfiguredConverter && collectionInfo is null
4✔
31
                        && type is INamedTypeSymbol namedType)
4✔
32
                {
33
                        objectInfo = CreateObjectModel(namedType);
4✔
34
                }
35

36
                return new JsonTypeRenderModel
4✔
37
                {
4✔
38
                        Index = index,
4✔
39
                        TypeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
40
                        IsValueType = type.IsValueType,
4✔
41
                        ConverterName = converterName,
4✔
42
                        IsEnum = isEnum,
4✔
43
                        UsesConfiguredConverter = usesConfiguredConverter,
4✔
44
                        CollectionInfo = collectionInfo,
4✔
45
                        ObjectInfo = objectInfo,
4✔
46
                };
4✔
47
        }
48

49
        private static JsonCollectionRenderModel? CreateCollectionModel(ITypeSymbol type)
50
        {
51
                return type switch
4✔
52
                {
4✔
53
                        IArrayTypeSymbol { Rank: 1, ElementType: { } elementType } => new()
4✔
54
                        {
4✔
55
                                CollectionType = "Array",
4✔
56
                                ElementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
57
                                KeyTypeName = null,
4✔
58
                        },
4✔
59

4✔
60
                        INamedTypeSymbol
4✔
61
                        {
4✔
62
                                Name: ("List" or "IList" or "IEnumerable") and var name,
4✔
63
                                Arity: 1,
4✔
64
                                ContainingNamespace.IsSystemCollectionsGeneric: true,
4✔
65
                                TypeArguments: [{ } elementType],
4✔
66
                        } => new()
4✔
67
                        {
4✔
68
                                CollectionType = name,
4✔
69
                                ElementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
70
                                KeyTypeName = null,
4✔
71
                        },
4✔
72

4✔
73
                        INamedTypeSymbol
4✔
74
                        {
4✔
75
                                Name: "IReadOnlyList",
4✔
76
                                Arity: 1,
4✔
77
                                ContainingNamespace.IsSystemCollectionsGeneric: true,
4✔
78
                                TypeArguments: [{ } elementType],
4✔
79
                        } => new()
4✔
80
                        {
4✔
81
                                CollectionType = "IEnumerable",
4✔
82
                                ElementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
83
                                KeyTypeName = null,
4✔
84
                        },
4✔
85

4✔
86
                        INamedTypeSymbol
4✔
87
                        {
4✔
88
                                Name: ("Dictionary" or "IDictionary" or "IReadOnlyDictionary") and var name,
4✔
89
                                Arity: 2,
4✔
90
                                ContainingNamespace.IsSystemCollectionsGeneric: true,
4✔
91
                                TypeArguments: [{ } keyType, { } elementType],
4✔
92
                        } => new()
4✔
93
                        {
4✔
94
                                CollectionType = name,
4✔
95
                                ElementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
96
                                KeyTypeName = keyType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
97
                        },
4✔
98

4✔
99
                        _ => null,
4✔
100
                };
4✔
101
        }
102

103
        private static JsonObjectRenderModel CreateObjectModel(INamedTypeSymbol type)
104
        {
105
                var members = GetMembers(type);
4✔
106
                var constructor = GetConstructor(type, members);
4✔
107
                var constructorParameters = constructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
4✔
108

109
                return new()
4✔
110
                {
4✔
111
                        HasParameterlessCreator = constructor is { Parameters: [] },
4✔
112
                        HasParameterizedCreator = constructor is { Parameters.Length: > 0 },
4✔
113

4✔
114
                        ConstructorParameters = constructorParameters
4✔
115
                                .Select((parameter, index) => new JsonConstructorParameterRenderModel
4✔
116
                                {
4✔
117
                                        Index = index,
4✔
118
                                        NameLiteral = Literal(parameter.Name),
4✔
119
                                        TypeName = parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
120
                                        HasDefaultValueLiteral = parameter.HasExplicitDefaultValue ? "true" : "false",
4✔
121
                                })
4✔
122
                                .ToEquatableReadOnlyList(),
4✔
123

4✔
124
                        Members = members
4✔
125
                                .Select(member =>
4✔
126
                                {
4✔
127
                                        var memberType = GetMemberType(member);
4✔
128
                                        return new JsonMemberRenderModel
4✔
129
                                        {
4✔
130
                                                Name = Escape(member.Name),
4✔
131
                                                NameLiteral = Literal(member.Name),
4✔
132
                                                TypeName = memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
133
                                                IsPropertyLiteral = member is IPropertySymbol ? "true" : "false",
4✔
134
                                                CanSet = CanSet(member),
4✔
135
                                                IsConstructorBound = constructor is not null && ConstructorContains(constructor, member.Name),
4✔
136
                                        };
4✔
137
                                })
4✔
138
                                .ToEquatableReadOnlyList(),
4✔
139
                };
4✔
140
        }
141

142
        private static HashSet<ITypeSymbol> CollectTypes(IEnumerable<ITypeSymbol> roots)
143
        {
144
                var visited = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
4✔
145
                void Visit(ITypeSymbol type)
146
                {
147
                        if (!visited.Add(type))
4✔
148
                                return;
4✔
149

150
                        if (CreateCollectionModel(type) is not null)
4✔
151
                        {
152
                                if (type is IArrayTypeSymbol array)
4✔
153
                                {
154
                                        Visit(array.ElementType);
4✔
155
                                }
156
                                else if (type is INamedTypeSymbol namedCollection)
4✔
157
                                {
158
                                        foreach (var argument in namedCollection.TypeArguments)
4✔
159
                                                Visit(argument);
4✔
160
                                }
161

162
                                return;
4✔
163
                        }
164

165
                        if (type is INamedTypeSymbol { TypeKind: not TypeKind.Enum, RootNamespace: not "NodaTime" } named && GetConverter(named) is null)
4✔
166
                        {
167
                                foreach (var member in GetMembers(named))
4✔
168
                                        Visit(GetMemberType(member));
4✔
169
                        }
170
                }
4✔
171

172
                foreach (var root in roots)
4✔
173
                        Visit(root);
4✔
174

175
                return visited;
4✔
176
        }
177

178
        private static List<ISymbol> GetMembers(INamedTypeSymbol type)
179
        {
180
                return type.GetMembers()
4✔
181
                        .Where(s => s is IPropertySymbol or IFieldSymbol)
4✔
182
                        .Where(ips => ips is
4✔
183
                        {
4✔
184
                                IsStatic: false,
4✔
185
                                DeclaredAccessibility: Accessibility.Public,
4✔
186
                        })
4✔
187
                        .Where(ips => ips is
4✔
188
                                IPropertySymbol
4✔
189
                                {
4✔
190
                                        GetMethod: not null,
4✔
191
                                        IsJobDetailsMember: false,
4✔
192
                                }
4✔
193
                                or IFieldSymbol { IsImplicitlyDeclared: true }
4✔
194
                        )
4✔
195
                        .ToList();
4✔
196
        }
197

198
        private static IMethodSymbol? GetConstructor(INamedTypeSymbol type, List<ISymbol> members) =>
199
                type.InstanceConstructors
4✔
200
                        .Where(c => c.DeclaredAccessibility == Accessibility.Public)
4✔
201
                        .Where(c => c.Parameters.All(p => members.Any(member => string.Equals(member.Name, p.Name, StringComparison.OrdinalIgnoreCase))))
4✔
202
                        .OrderByDescending(c => c.Parameters.Length)
4✔
203
                        .FirstOrDefault();
4✔
204

205
        private static bool ConstructorContains(IMethodSymbol constructor, string name) =>
206
                constructor.Parameters.Any(parameter => string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase));
4✔
207

208
        private static ITypeSymbol GetMemberType(ISymbol member) =>
209
                member is IPropertySymbol property ? property.Type : ((IFieldSymbol)member).Type;
4✔
210

211
        private static bool CanSet(ISymbol member) => member switch
4✔
212
        {
4✔
213
                IPropertySymbol property => property.SetMethod is { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false },
4✔
UNCOV
214
                IFieldSymbol field => !field.IsReadOnly && !field.IsConst,
×
UNCOV
215
                _ => false,
×
216
        };
4✔
217

218
        private static string? GetConverter(ITypeSymbol type)
219
        {
220
                if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte })
4✔
UNCOV
221
                        return "ByteArrayConverter";
×
222

223
                if (type.SpecialType == SpecialType.System_Boolean) return "BooleanConverter";
4✔
224
                if (type.SpecialType == SpecialType.System_Byte) return "ByteConverter";
4✔
225
                if (type.SpecialType == SpecialType.System_SByte) return "SByteConverter";
4✔
226
                if (type.SpecialType == SpecialType.System_Int16) return "Int16Converter";
4✔
227
                if (type.SpecialType == SpecialType.System_UInt16) return "UInt16Converter";
4✔
228
                if (type.SpecialType == SpecialType.System_Int32) return "Int32Converter";
4✔
229
                if (type.SpecialType == SpecialType.System_UInt32) return "UInt32Converter";
4✔
230
                if (type.SpecialType == SpecialType.System_Int64) return "Int64Converter";
4✔
231
                if (type.SpecialType == SpecialType.System_UInt64) return "UInt64Converter";
4✔
232
                if (type.SpecialType == SpecialType.System_Char) return "CharConverter";
4✔
233
                if (type.SpecialType == SpecialType.System_Single) return "SingleConverter";
4✔
234
                if (type.SpecialType == SpecialType.System_Double) return "DoubleConverter";
4✔
235
                if (type.SpecialType == SpecialType.System_Decimal) return "DecimalConverter";
4✔
236
                if (type.SpecialType == SpecialType.System_String) return "StringConverter";
4✔
237
                if (type.SpecialType == SpecialType.System_DateTime) return "DateTimeConverter";
4✔
238
                if (type.SpecialType == SpecialType.System_Object) return "ObjectConverter";
4✔
239

240
                if (type is not INamedTypeSymbol
4✔
241
                        {
4✔
242
                                Arity: 0,
4✔
243
                                ContainingNamespace.IsSystem: true,
4✔
244
                        })
4✔
245
                {
246
                        return null;
4✔
247
                }
248

249
                return type.Name switch
4✔
250
                {
4✔
251
                        "Guid" => "GuidConverter",
4✔
UNCOV
252
                        "DateTimeOffset" => "DateTimeOffsetConverter",
×
UNCOV
253
                        "TimeSpan" => "TimeSpanConverter",
×
UNCOV
254
                        "DateOnly" => "DateOnlyConverter",
×
UNCOV
255
                        "TimeOnly" => "TimeOnlyConverter",
×
UNCOV
256
                        "Uri" => "UriConverter",
×
UNCOV
257
                        "Version" => "VersionConverter",
×
UNCOV
258
                        _ => null,
×
259
                };
4✔
260
        }
261

262
        private static string Literal(string value) =>
263
                Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(value, quote: true);
4✔
264

265
        private static string Escape(string value) =>
266
                Microsoft.CodeAnalysis.CSharp.SyntaxFacts.GetKeywordKind(value) == Microsoft.CodeAnalysis.CSharp.SyntaxKind.None
4✔
267
                        ? value
4✔
268
                        : "@" + value;
4✔
269
}
270

271
internal sealed record JsonMetadataRenderModel
272
{
273
        public required string PayloadTypeName { get; init; }
274
        public required EquatableReadOnlyList<JsonTypeRenderModel> Types { get; init; }
275
}
276

277
internal sealed record JsonTypeRenderModel
278
{
279
        public required int Index { get; init; }
280
        public required string TypeName { get; init; }
281
        public required bool IsValueType { get; init; }
282
        public required string? ConverterName { get; init; }
283
        public required bool IsEnum { get; init; }
284
        public required bool UsesConfiguredConverter { get; init; }
285
        public required JsonCollectionRenderModel? CollectionInfo { get; init; }
286
        public required JsonObjectRenderModel? ObjectInfo { get; init; }
287
}
288

289
internal sealed record JsonCollectionRenderModel
290
{
291
        public required string CollectionType { get; init; }
292
        public required string ElementTypeName { get; init; }
293
        public required string? KeyTypeName { get; init; }
294
}
295

296
internal sealed record JsonObjectRenderModel
297
{
298
        public required bool HasParameterlessCreator { get; init; }
299
        public required bool HasParameterizedCreator { get; init; }
300
        public required EquatableReadOnlyList<JsonConstructorParameterRenderModel> ConstructorParameters { get; init; }
301
        public required EquatableReadOnlyList<JsonMemberRenderModel> Members { get; init; }
302
}
303

304
internal sealed record JsonConstructorParameterRenderModel
305
{
306
        public required int Index { get; init; }
307
        public required string NameLiteral { get; init; }
308
        public required string TypeName { get; init; }
309
        public required string HasDefaultValueLiteral { get; init; }
310
}
311

312
internal sealed record JsonMemberRenderModel
313
{
314
        public required string Name { get; init; }
315
        public required string NameLiteral { get; init; }
316
        public required string TypeName { get; init; }
317
        public required string IsPropertyLiteral { get; init; }
318
        public required bool CanSet { get; init; }
319
        public required bool IsConstructorBound { get; init; }
320
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc