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

xoofx / Tomlyn / 13785706002

11 Mar 2025 09:55AM UTC coverage: 82.006% (-0.2%) from 82.169%
13785706002

push

github

xoofx
Simplify serialization and inlining heuristic (Fixes #98)

1984 of 2704 branches covered (73.37%)

Branch coverage included in aggregate %.

107 of 107 new or added lines in 3 files covered. (100.0%)

13 existing lines in 1 file now uncovered.

4237 of 4882 relevant lines covered (86.79%)

1365.26 hits per line

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

93.26
/src/Tomlyn/Model/ModelToTomlTransform.cs
1
// Copyright (c) Alexandre Mutel. All rights reserved.
2
// Licensed under the BSD-Clause 2 license.
3
// See license.txt file in the project root for full license information.
4

5
using System;
6
using System.Collections.Generic;
7
using System.IO;
8
using Tomlyn.Helpers;
9
using Tomlyn.Model.Accessors;
10
using Tomlyn.Syntax;
11
using Tomlyn.Text;
12

13
namespace Tomlyn.Model;
14

15
internal class ModelToTomlTransform
16
{
17
    private readonly object _rootObject;
18
    private readonly DynamicModelWriteContext _context;
19
    private readonly TextWriter _writer;
20
    private readonly List<ObjectPath> _paths;
21
    private readonly List<ObjectPath> _currentPaths;
22
    private readonly Stack<List<KeyValueAccessor>> _tempPropertiesStack;
23
    private ITomlMetadataProvider? _metadataProvider;
24

25
    public ModelToTomlTransform(object rootObject, DynamicModelWriteContext context)
26
    {
27
        _rootObject = rootObject;
149✔
28
        _context = context;
149✔
29
        _writer = context.Writer;
149✔
30
        _paths = new List<ObjectPath>();
149✔
31
        _tempPropertiesStack = new Stack<List<KeyValueAccessor>>();
149✔
32
        _currentPaths = new List<ObjectPath>();
149✔
33
    }
149✔
34

35
    public void Run()
36
    {
37
        var itemAccessor = _context.GetAccessor(_rootObject.GetType());
149✔
38
        if (itemAccessor is ObjectDynamicAccessor objectDynamicAccessor)
149!
39
        {
40
            VisitObject(objectDynamicAccessor, _rootObject, false);
149✔
41
        }
42
        else
43
        {
44
            _context.Diagnostics.Error(new SourceSpan(), $"The root object must a class with properties or a dictionary. Cannot be of kind {itemAccessor}.");
×
45
        }
46
    }
×
47

48
    private void PushName(string name, bool isTableArray)
49
    {
50
        _paths.Add(new ObjectPath(name, isTableArray));
199✔
51
    }
199✔
52

53
    private void WriteHeaderTable()
54
    {
55
        var name = _paths[_paths.Count - 1].Name;
169✔
56
        WriteLeadingTrivia(name);
169✔
57
        _writer.Write("[");
169✔
58
        WriteDottedKeys();
169✔
59
        _writer.Write("]");
169✔
60
        WriteTrailingTrivia(name);
169✔
61
        _writer.WriteLine();
169✔
62
        WriteTrailingTriviaAfterEndOfLine(name);
169✔
63
        _currentPaths.Clear();
169✔
64
        _currentPaths.AddRange(_paths);
169✔
65
    }
169✔
66

67
    private void WriteHeaderTableArray()
68
    {
69
        var name = _paths[_paths.Count - 1].Name;
59✔
70
        WriteLeadingTrivia(name);
59✔
71
        _writer.Write("[[");
59✔
72
        WriteDottedKeys();
59✔
73
        _writer.Write("]]");
59✔
74
        WriteTrailingTrivia(name);
59✔
75
        _writer.WriteLine();
59✔
76
        WriteTrailingTriviaAfterEndOfLine(name);
59✔
77
        _currentPaths.Clear();
59✔
78
        _currentPaths.AddRange(_paths);
59✔
79
    }
59✔
80

81
    private void WriteDottedKeys()
82
    {
83
        bool isFirst = true;
228✔
84
        foreach (var name in _paths)
1,252✔
85
        {
86
            if (!isFirst)
398✔
87
            {
88
                _writer.Write(".");
170✔
89
            }
90

91
            WriteKey(name.Name);
398✔
92
            isFirst = false;
398✔
93
        }
94
    }
228✔
95

96
    private void WriteKey(string name)
97
    {
98
        _writer.Write(EscapeKey(name));
1,041✔
99
    }
1,041✔
100

101
    private string EscapeKey(string name)
102
    {
103
        if (string.IsNullOrWhiteSpace(name)) return $"\"{name.EscapeForToml()}\"";
1,043✔
104

105
        // A-Za-z0-9_-
106
        foreach (var c in name)
13,413✔
107
        {
108
            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-') || c == '.')
5,681✔
109
            {
110
                return $"\"{name.EscapeForToml()}\"";
27✔
111
            }
112
        }
113

114
        return name;
1,012✔
115
    }
116

117
    private void EnsureScope()
118
    {
119
        if (IsCurrentScopeValid()) return;
1,172!
120

UNCOV
121
        if (_paths.Count == 0)
×
122
        {
123
            _currentPaths.Clear();
×
124
        }
125
        else
126
        {
UNCOV
127
            var lastObjectPath = _paths[_paths.Count - 1];
×
128

UNCOV
129
            if (lastObjectPath.IsTableArray)
×
130
            {
131
                WriteHeaderTableArray();
×
132
            }
133
            else
134
            {
UNCOV
135
                WriteHeaderTable();
×
136
            }
137
        }
UNCOV
138
    }
×
139

140
    private bool IsCurrentScopeValid()
141
    {
142
        if (_paths.Count == _currentPaths.Count)
586!
143
        {
144
            for (var index = 0; index < _paths.Count; index++)
1,782✔
145
            {
146
                var path1 = _paths[index];
305✔
147
                var path2 = _currentPaths[index];
305✔
148
                if (!path1.Equals(path2)) return false;
305!
149
            }
150

151
            return true;
586✔
152
        }
153

UNCOV
154
        return false;
×
155
    }
156

157

158
    private void PopName()
159
    {
160
        _paths.RemoveAt(_paths.Count - 1);
199✔
161
    }
199✔
162

163

164
    private List<KeyValueAccessor> RentTempProperties()
165
    {
166
        return _tempPropertiesStack.Count > 0 ? _tempPropertiesStack.Pop() : new List<KeyValueAccessor>();
525✔
167
    }
168

169
    private void ReleaseTempProperties(List<KeyValueAccessor> tempProperties)
170
    {
171
        tempProperties.Clear();
525✔
172
        _tempPropertiesStack.Push(tempProperties);
525✔
173
    }
525✔
174

175
    private bool VisitObject(ObjectDynamicAccessor accessor, object currentObject, bool inline)
176
    {
177
        bool hasElements = false;
383✔
178

179
        var previousMetadata = _metadataProvider;
383✔
180
        _metadataProvider = currentObject as ITomlMetadataProvider;
383✔
181
        var properties = RentTempProperties();
383✔
182
        try
183
        {
184
            // Pre-convert values to TOML values
185
            var convertToToml = _context.ConvertToToml;
383✔
186
            if (convertToToml != null)
383✔
187
            {
188
                foreach (var property in accessor.GetProperties(currentObject))
4✔
189
                {
190
                    // Allow to convert a value to a TOML simpler value before serializing.
191
                    var value = property.Value;
1✔
192
                    if (value is not null)
1✔
193
                    {
194
                        var result = convertToToml(value);
1✔
195
                        if (result != null)
1✔
196
                        {
197
                            value = result;
1✔
198
                        }
199
                        properties.Add(new KeyValueAccessor(property.Key, value, _context.GetAccessor(value.GetType())));
1✔
200
                    }
201
                }
202
            }
203
            else
204
            {
205
                foreach (var property in accessor.GetProperties(currentObject))
2,526✔
206
                {
207
                    var value = property.Value;
881✔
208
                    if (value is not null)
881✔
209
                    {
210
                        properties.Add(new KeyValueAccessor(property.Key, value, _context.GetAccessor(value.GetType())));
841✔
211
                    }
212
                }
213
            }
214

215
            if (inline)
383✔
216
            {
217
                // Write all properties inlined
218
                for (var i = 0; i < properties.Count; i++)
26✔
219
                {
220
                    var prop = properties[i];
7✔
221
                    if (i > 0)
7✔
222
                    {
223
                        _writer.Write(", ");
2✔
224
                    }
225

226
                    WriteKeyValue(prop, true);
7✔
227
                    hasElements = true;
7✔
228
                }
229
            }
230
            else
231
            {
232
                // We have a mix of inlined and non-inlined properties
233
                // Write always inlined properties: primitives and other objects that require to be inlined
234
                List<KeyValueAccessor>? nonInlinedProperties = null;
377✔
235
                foreach (var prop in properties)
2,424✔
236
                {
237
                    var propDisplayKind = GetDisplayKind(prop.Key);
835✔
238
                    if (prop.Accessor is not PrimitiveDynamicAccessor && (propDisplayKind == TomlPropertyDisplayKind.NoInline || !IsRequiringInline(prop)))
835✔
239
                    {
240
                        nonInlinedProperties ??= RentTempProperties();
199✔
241
                        nonInlinedProperties.Add(prop);
199✔
242
                        continue;
199✔
243
                    }
244
                    var name = prop.Key;
636✔
245
                    WriteLeadingTrivia(name);
636✔
246
                    WriteKeyValue(prop, true);
636✔
247

248
                    WriteTrailingTrivia(name);
636✔
249
                    _writer.WriteLine();
636✔
250
                    WriteTrailingTriviaAfterEndOfLine(name);
636✔
251
                    hasElements = true;
636✔
252
                }
253

254
                // Write non-inlined properties
255
                if (nonInlinedProperties is not null)
377✔
256
                {
257
                    foreach (var prop in nonInlinedProperties)
682✔
258
                    {
259
                        var name = prop.Key;
199✔
260
                        WriteLeadingTrivia(name);
199✔
261

262
                        WriteKeyValue(prop, false);
199✔
263

264
                        if (prop.Accessor is not ObjectDynamicAccessor && prop.Value is not TomlTableArray)
199✔
265
                        {
266
                            WriteTrailingTrivia(name);
15✔
267
                            _writer.WriteLine();
15✔
268
                            WriteTrailingTriviaAfterEndOfLine(name);
15✔
269
                        }
270

271
                        hasElements = true;
199✔
272
                    }
273

274
                    ReleaseTempProperties(nonInlinedProperties);
142✔
275
                }
276
            }
277
        }
377✔
278
        finally
279
        {
280
            _metadataProvider = previousMetadata;
383✔
281
            ReleaseTempProperties(properties);
383✔
282
        }
383✔
283

284
        return hasElements;
383✔
285
    }
286

287
    private void VisitList(ListDynamicAccessor accessor, object currentObject, bool inline)
288
    {
289
        bool isFirst = true;
112✔
290
        foreach (var value in accessor.GetElements(currentObject))
628✔
291
        {
292
            // Skip any null value
293
            if (value is null) continue; // TODO: should emit an error?
202✔
294

295
            var itemAccessor = _context.GetAccessor(value.GetType());
202✔
296

297
            if (inline)
202✔
298
            {
299
                if (!isFirst)
143✔
300
                {
301
                    _writer.Write(", ");
72✔
302
                }
303

304
                WriteValueInline(itemAccessor, value);
143✔
305
                isFirst = false;
143✔
306
            }
307
            else
308
            {
309
                var previousMetadata = _metadataProvider;
59✔
310
                try
311
                {
312
                    _metadataProvider = value as ITomlMetadataProvider;
59✔
313
                    WriteHeaderTableArray();
59✔
314
                }
59✔
315
                finally
316
                {
317
                    _metadataProvider = previousMetadata;
59✔
318
                }
59✔
319
                VisitObject((ObjectDynamicAccessor)itemAccessor, value, false);
59✔
320
            }
321
        }
322
    }
112✔
323

324
    private void WriteKeyValue(in KeyValueAccessor keyValueAccessor, bool inline)
325
    {
326
        var name = keyValueAccessor.Key;
842✔
327
        var value = keyValueAccessor.Value;
842✔
328
        var accessor = keyValueAccessor.Accessor;
842!
329

330
        switch (accessor)
331
        {
332
            case ListDynamicAccessor listDynamicAccessor:
333
            {
334
                if (inline)
87✔
335
                {
336
                    WriteKey(name);
57✔
337
                    _writer.Write(" = [");
57✔
338
                    VisitList(listDynamicAccessor, value, true);
57✔
339
                    _writer.Write("]");
57✔
340
                }
341
                else
342
                {
343
                    PushName(name, true);
30✔
344
                    VisitList(listDynamicAccessor, value, false);
30✔
345
                    PopName();
30✔
346
                }
347
            }
348
                break;
30✔
349
            case ObjectDynamicAccessor objectAccessor:
350
                if (inline)
169!
351
                {
UNCOV
352
                    WriteKey(name);
×
UNCOV
353
                    _writer.Write(" = {");
×
UNCOV
354
                    VisitObject(objectAccessor, value, true);
×
UNCOV
355
                    _writer.Write("}");
×
356
                }
357
                else
358
                {
359
                    PushName(name, false);
169✔
360
                    var previousMetadataProvider = _metadataProvider;
169✔
361
                    _metadataProvider = value as ITomlMetadataProvider;
169✔
362
                    try
363
                    {
364
                        WriteHeaderTable();
169✔
365
                    }
169✔
366
                    finally
367
                    {
368
                        _metadataProvider = previousMetadataProvider;
169✔
369
                    }
169✔
370
                    VisitObject(objectAccessor, value, false);
169✔
371
                    PopName();
169✔
372
                }
373
                break;
169✔
374
            case PrimitiveDynamicAccessor primitiveDynamicAccessor:
375
                EnsureScope();
586✔
376
                WriteKey(name);
586✔
377
                _writer.Write(" = ");
586✔
378
                WritePrimitive(value, GetDisplayKind(name));
586✔
379
                break;
586✔
380
            default:
UNCOV
381
                throw new ArgumentOutOfRangeException(nameof(accessor));
×
382
        }
383
    }
384

385
    private TomlPropertyDisplayKind GetDisplayKind(string name)
386
    {
387
        var kind = TomlPropertyDisplayKind.Default;
1,421✔
388
        if (_metadataProvider is not null && _metadataProvider.PropertiesMetadata is not null && _metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata))
1,421✔
389
        {
390
            kind = propertyMetadata.DisplayKind;
771✔
391
        }
392

393
        return kind;
1,421✔
394
    }
395

396
    private bool IsRequiringInline(in KeyValueAccessor prop)
397
    {
398
        if (prop.Accessor is ListDynamicAccessor listDynamicAccessor && prop.Value is not TomlTableArray)
256✔
399
        {
400
            bool hasOnlyObjects = true;
72✔
401
            bool hasElements = false;
72✔
402
            foreach (var element in listDynamicAccessor.GetElements(prop.Value))
428✔
403
            {
404
                if (element is null) continue; // TODO should this be an error?
142✔
405
                var elementAccessor = _context.GetAccessor(element.GetType());
142✔
406

407
                if (elementAccessor is not ObjectDynamicAccessor)
142✔
408
                {
409
                    hasOnlyObjects = false;
106✔
410
                }
411

412
                hasElements = true;
142✔
413
            }
414
            
415
            return !hasElements || !hasOnlyObjects;
72✔
416
        }
417

418
        return false;
184✔
419
    }
420

421
    private void WriteLeadingTrivia(string name)
422
    {
423
        if (_metadataProvider?.PropertiesMetadata is null || !_metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata) || propertyMetadata.LeadingTrivia is null) return;
2,113✔
424

425
        foreach (var trivia in propertyMetadata.LeadingTrivia)
126✔
426
        {
427
            if (trivia.Text is not null) _writer.Write(trivia.Text);
100✔
428
        }
429
    }
13✔
430

431
    private void WriteTrailingTrivia(string name)
432
    {
433
        if (_metadataProvider?.PropertiesMetadata is null || !_metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata) || propertyMetadata.TrailingTrivia is null) return;
1,712✔
434

435
        foreach (var trivia in propertyMetadata.TrailingTrivia)
216✔
436
        {
437
            if (trivia.Text is not null) _writer.Write(trivia.Text);
124✔
438
        }
439
    }
46✔
440

441
    private void WriteTrailingTriviaAfterEndOfLine(string name)
442
    {
443
        if (_metadataProvider?.PropertiesMetadata is null || !_metadataProvider.PropertiesMetadata.TryGetProperty(name, out var propertyMetadata) || propertyMetadata.TrailingTriviaAfterEndOfLine is null) return;
1,652✔
444

445
        foreach (var trivia in propertyMetadata.TrailingTriviaAfterEndOfLine)
532✔
446
        {
447
            if (trivia.Text is not null) _writer.Write(trivia.Text);
320✔
448
        }
449
    }
106✔
450

451
    private void WriteValueInline(DynamicAccessor accessor, object? value)
452
    {
453
        if (value is null) return;
143!
454

455
        switch (accessor)
456
        {
457
            case ListDynamicAccessor listDynamicAccessor:
458
                _writer.Write("[");
25✔
459
                    VisitList(listDynamicAccessor, value, true);
25✔
460
                _writer.Write("]");
25✔
461
                break;
25✔
462
            case ObjectDynamicAccessor objectAccessor:
463
                _writer.Write("{");
6✔
464
                VisitObject(objectAccessor, value, true);
6✔
465
                _writer.Write("}");
6✔
466
                break;
6✔
467
            case PrimitiveDynamicAccessor primitiveDynamicAccessor:
468
                WritePrimitive(value, TomlPropertyDisplayKind.Default);
112✔
469
                break;
112✔
470
            default:
UNCOV
471
                throw new ArgumentOutOfRangeException(nameof(accessor));
×
472
        }
473
    }
474

475

476
    private void WritePrimitive(object primitive, TomlPropertyDisplayKind displayKind)
477
    {
478
        if (primitive is bool b)
698✔
479
        {
480
            _writer.Write(TomlFormatHelper.ToString(b));
30✔
481
        }
482
        else if (primitive is string s)
668✔
483
        {
484
            _writer.Write(TomlFormatHelper.ToString(s, displayKind));
260✔
485
        }
486
        else if (primitive is int i32)
408✔
487
        {
488
            _writer.Write(TomlFormatHelper.ToString(i32, displayKind));
30✔
489
        }
490
        else if (primitive is long i64)
378✔
491
        {
492
            _writer.Write(TomlFormatHelper.ToString(i64, displayKind));
164✔
493
        }
494
        else if (primitive is uint u32)
214✔
495
        {
496
            _writer.Write(TomlFormatHelper.ToString(u32, displayKind));
8✔
497
        }
498
        else if (primitive is ulong u64)
206✔
499
        {
500
            _writer.Write(TomlFormatHelper.ToString(u64, displayKind));
8✔
501
        }
502
        else if (primitive is sbyte i8)
198✔
503
        {
504
            _writer.Write(TomlFormatHelper.ToString(i8, displayKind));
8✔
505
        }
506
        else if (primitive is byte u8)
190✔
507
        {
508
            _writer.Write(TomlFormatHelper.ToString(u8, displayKind));
8✔
509
        }
510
        else if (primitive is short i16)
182✔
511
        {
512
            _writer.Write(TomlFormatHelper.ToString(i16, displayKind));
8✔
513
        }
514
        else if (primitive is ushort u16)
174✔
515
        {
516
            _writer.Write(TomlFormatHelper.ToString(u16, displayKind));
8✔
517
        }
518
        else if (primitive is float f32)
166✔
519
        {
520
            _writer.Write(TomlFormatHelper.ToString(f32));
28✔
521
        }
522
        else if (primitive is double f64)
138✔
523
        {
524
            _writer.Write(TomlFormatHelper.ToString(f64));
73✔
525
        }
526
        else if (primitive is TomlDateTime tomlDateTime)
65✔
527
        {
528
            _writer.Write(TomlFormatHelper.ToString(tomlDateTime));
32✔
529
        }
530
        else if (primitive is DateTime dateTime)
33✔
531
        {
532
            _writer.Write(TomlFormatHelper.ToString(dateTime, displayKind));
8✔
533
        }
534
        else if (primitive is DateTimeOffset dateTimeOffset)
25✔
535
        {
536
            _writer.Write(TomlFormatHelper.ToString(dateTimeOffset, displayKind));
8✔
537
        }
538
        else if (primitive is Enum enumValue)
17✔
539
        {
540
            _writer.Write(TomlFormatHelper.ToString(enumValue.ToString(), displayKind));
1✔
541
        }
542
#if NET6_0_OR_GREATER
543
        else if (primitive is DateOnly dateOnly)
16✔
544
        {
545
            _writer.Write(TomlFormatHelper.ToString(dateOnly, displayKind));
8✔
546
        }
547
        else if (primitive is TimeOnly timeOnly)
8!
548
        {
549
            _writer.Write(TomlFormatHelper.ToString(timeOnly, displayKind));
8✔
550
        }
551
#endif
552
        else
553
        {
554
            // Unexpected
UNCOV
555
            throw new InvalidOperationException($"Invalid primitive {primitive.GetType().FullName}");
×
556
        }
557
    }
558

559
    private record struct ObjectPath(string Name, bool IsTableArray);
560
    
561
    private readonly record struct KeyValueAccessor(string Key, object Value, DynamicAccessor Accessor);
562
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc