Skip to content

Commit 7ce8fd0

Browse files
Fix empty collection serialization, depth-exceeded indent, and [] / {} round-tripping
- Fix if/else expressions that collapsed empty arrays to $null (PowerShell unrolls @() to nothing in expression output). Converted to statement form in ConvertTo-YamlMapping, ConvertTo-YamlSequence, ConvertTo-YamlNode, Get-YamlMappingPair, and ConvertTo-Yaml. - Add indentation to empty {} and [] output in ConvertTo-YamlMapping and ConvertTo-YamlSequence so nested empty collections serialize correctly. - Add indentation to depth-exceeded branch in ConvertTo-YamlNode. - Add inline empty-collection checks in ConvertTo-YamlSequence for nested mapping/sequence values within sequence-item-mappings. - Recognize {} and [] inline literals in ConvertFrom-YamlMapping and ConvertFrom-YamlSequence to restore round-trip fidelity. - Add 17 new tests: empty collection rendering, depth-exceeded indentation, {} / [] parsing, and round-trip preservation of empty collections.
1 parent 8956c19 commit 7ce8fd0

9 files changed

Lines changed: 178 additions & 26 deletions

src/functions/private/ConvertFrom-YamlMapping.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@
4343
$Context.Index++
4444

4545
if ($rest.Length -gt 0) {
46-
$map[$key] = ConvertFrom-YamlScalar -Raw $rest
46+
if ($rest -ceq '{}') {
47+
$map[$key] = if ($Context.AsHashtable) { [ordered]@{} } else { [pscustomobject][ordered]@{} }
48+
} elseif ($rest -ceq '[]') {
49+
$map[$key] = @()
50+
} else {
51+
$map[$key] = ConvertFrom-YamlScalar -Raw $rest
52+
}
4753
continue
4854
}
4955

src/functions/private/ConvertFrom-YamlSequence.ps1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@
6464
continue
6565
}
6666

67+
# Empty collection literals (flow-style shorthand).
68+
if ($afterDash -ceq '{}') {
69+
$val = if ($Context.AsHashtable) { [ordered]@{} } else { [pscustomobject][ordered]@{} }
70+
$list.Add($val)
71+
$Context.Index++
72+
continue
73+
}
74+
if ($afterDash -ceq '[]') {
75+
$list.Add(@())
76+
$Context.Index++
77+
continue
78+
}
79+
6780
# Plain scalar element.
6881
$list.Add((ConvertFrom-YamlScalar -Raw $afterDash))
6982
$Context.Index++

src/functions/private/ConvertTo-YamlMapping.ps1

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
$pairs = Get-YamlMappingPair -Value $Value
1616
if ($pairs.Count -eq 0) {
17-
$null = $Builder.Append('{}').AppendLine()
17+
$indent = ' ' * ($Level * $Options.Indent)
18+
$null = $Builder.Append($indent).Append('{}').AppendLine()
1819
return
1920
}
2021

@@ -29,10 +30,10 @@
2930
continue
3031
}
3132

32-
$rawVal = if ($val -is [psobject] -and $null -ne $val.PSObject -and $null -ne $val.PSObject.BaseObject) {
33-
$val.PSObject.BaseObject
33+
if ($val -is [psobject] -and $null -ne $val.PSObject -and $null -ne $val.PSObject.BaseObject) {
34+
$rawVal = $val.PSObject.BaseObject
3435
} else {
35-
$val
36+
$rawVal = $val
3637
}
3738

3839
if (Test-YamlMappingType -Value $rawVal) {

src/functions/private/ConvertTo-YamlNode.ps1

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424

2525
if ($CurrentDepth -gt $Options.Depth) {
2626
$repr = if ($null -eq $Value) { 'null' } else { Format-YamlScalar -Value $Value.ToString() -Options $Options }
27-
$null = $Builder.Append($repr).AppendLine()
27+
$indent = ' ' * ($Level * $Options.Indent)
28+
$null = $Builder.Append($indent).Append($repr).AppendLine()
2829
return
2930
}
3031

@@ -34,10 +35,10 @@
3435
}
3536

3637
# Unwrap PSObject for type tests.
37-
$raw = if ($Value -is [psobject] -and $null -ne $Value.PSObject -and $null -ne $Value.PSObject.BaseObject) {
38-
$Value.PSObject.BaseObject
38+
if ($Value -is [psobject] -and $null -ne $Value.PSObject -and $null -ne $Value.PSObject.BaseObject) {
39+
$raw = $Value.PSObject.BaseObject
3940
} else {
40-
$Value
41+
$raw = $Value
4142
}
4243

4344
if (Test-YamlMappingType -Value $raw) {

src/functions/private/ConvertTo-YamlSequence.ps1

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@
1414

1515
$items = @($Value)
1616
if ($items.Count -eq 0) {
17-
$null = $Builder.Append('[]').AppendLine()
17+
$indent = ' ' * ($Level * $Options.Indent)
18+
$null = $Builder.Append($indent).Append('[]').AppendLine()
1819
return
1920
}
2021

2122
$indent = ' ' * ($Level * $Options.Indent)
2223

2324
foreach ($item in $items) {
24-
$rawItem = if ($item -is [psobject] -and $null -ne $item.PSObject -and $null -ne $item.PSObject.BaseObject) {
25-
$item.PSObject.BaseObject
25+
if ($item -is [psobject] -and $null -ne $item.PSObject -and $null -ne $item.PSObject.BaseObject) {
26+
$rawItem = $item.PSObject.BaseObject
2627
} else {
27-
$item
28+
$rawItem = $item
2829
}
2930

3031
if ($null -eq $item) {
@@ -46,10 +47,10 @@
4647
$first = $false
4748

4849
$val = $pair.Value
49-
$rawVal = if ($val -is [psobject] -and $null -ne $val.PSObject -and $null -ne $val.PSObject.BaseObject) {
50-
$val.PSObject.BaseObject
50+
if ($val -is [psobject] -and $null -ne $val.PSObject -and $null -ne $val.PSObject.BaseObject) {
51+
$rawVal = $val.PSObject.BaseObject
5152
} else {
52-
$val
53+
$rawVal = $val
5354
}
5455

5556
if ($null -eq $val) {
@@ -58,14 +59,24 @@
5859
}
5960

6061
if (Test-YamlMappingType -Value $rawVal) {
61-
$null = $Builder.Append($prefix).Append($keyText).Append(':').AppendLine()
62-
ConvertTo-YamlNode -Value $val -Builder $Builder -Level ($Level + 2) -CurrentDepth ($CurrentDepth + 1) -Options $Options
62+
$childPairs = Get-YamlMappingPair -Value $val
63+
if ($childPairs.Count -eq 0) {
64+
$null = $Builder.Append($prefix).Append($keyText).Append(': {}').AppendLine()
65+
} else {
66+
$null = $Builder.Append($prefix).Append($keyText).Append(':').AppendLine()
67+
ConvertTo-YamlNode -Value $val -Builder $Builder -Level ($Level + 2) -CurrentDepth ($CurrentDepth + 1) -Options $Options
68+
}
6369
continue
6470
}
6571

6672
if (Test-YamlSequenceType -Value $rawVal) {
67-
$null = $Builder.Append($prefix).Append($keyText).Append(':').AppendLine()
68-
ConvertTo-YamlSequence -Value $rawVal -Builder $Builder -Level ($Level + 2) -CurrentDepth ($CurrentDepth + 1) -Options $Options
73+
$arr = @($rawVal)
74+
if ($arr.Count -eq 0) {
75+
$null = $Builder.Append($prefix).Append($keyText).Append(': []').AppendLine()
76+
} else {
77+
$null = $Builder.Append($prefix).Append($keyText).Append(':').AppendLine()
78+
ConvertTo-YamlSequence -Value $rawVal -Builder $Builder -Level ($Level + 2) -CurrentDepth ($CurrentDepth + 1) -Options $Options
79+
}
6980
continue
7081
}
7182

@@ -76,8 +87,13 @@
7687
}
7788

7889
if (Test-YamlSequenceType -Value $rawItem) {
79-
$null = $Builder.Append($indent).Append('-').AppendLine()
80-
ConvertTo-YamlSequence -Value $rawItem -Builder $Builder -Level ($Level + 1) -CurrentDepth ($CurrentDepth + 1) -Options $Options
90+
$arr = @($rawItem)
91+
if ($arr.Count -eq 0) {
92+
$null = $Builder.Append($indent).Append('- []').AppendLine()
93+
} else {
94+
$null = $Builder.Append($indent).Append('-').AppendLine()
95+
ConvertTo-YamlSequence -Value $rawItem -Builder $Builder -Level ($Level + 1) -CurrentDepth ($CurrentDepth + 1) -Options $Options
96+
}
8197
continue
8298
}
8399

src/functions/private/Get-YamlMappingPair.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
)
1414

1515
$pairs = [System.Collections.Generic.List[pscustomobject]]::new()
16-
$raw = if ($Value -is [psobject] -and $null -ne $Value.PSObject -and $null -ne $Value.PSObject.BaseObject) {
17-
$Value.PSObject.BaseObject
16+
if ($Value -is [psobject] -and $null -ne $Value.PSObject -and $null -ne $Value.PSObject.BaseObject) {
17+
$raw = $Value.PSObject.BaseObject
1818
} else {
19-
$Value
19+
$raw = $Value
2020
}
2121

2222
if ($raw -is [System.Collections.IDictionary]) {

src/functions/public/ConvertTo-Yaml.ps1

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@
8383
if ($AsArray) {
8484
ConvertTo-YamlSequence -Value $items.ToArray() -Builder $sb -Level 0 -CurrentDepth 0 -Options $options
8585
} else {
86-
$root = if ($items.Count -eq 1) { $items[0] } else { $items.ToArray() }
86+
if ($items.Count -eq 1) {
87+
$root = $items[0]
88+
} else {
89+
$root = $items.ToArray()
90+
}
8791
ConvertTo-YamlNode -Value $root -Builder $sb -Level 0 -CurrentDepth 0 -Options $options
8892
}
8993
return $sb.ToString().TrimEnd("`r", "`n")

tests/ConvertFrom-Yaml.Tests.ps1

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,39 @@ a:
329329
Should -Be 'ConvertFrom-Yaml'
330330
}
331331
}
332+
333+
Context 'Empty collection literals' {
334+
It 'Parses {} as an empty mapping (PSCustomObject by default)' {
335+
$result = 'data: {}' | ConvertFrom-Yaml
336+
$result.data | Should -BeOfType [PSCustomObject]
337+
@($result.data.PSObject.Properties).Count | Should -Be 0
338+
}
339+
340+
It 'Parses {} as an empty OrderedDictionary with -AsHashtable' {
341+
$result = 'data: {}' | ConvertFrom-Yaml -AsHashtable
342+
$result['data'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary]
343+
$result['data'].Count | Should -Be 0
344+
}
345+
346+
It 'Parses [] as an empty array' {
347+
$result = 'items: []' | ConvertFrom-Yaml
348+
$result.items | Should -BeNullOrEmpty
349+
# Verify via round-trip that ConvertTo-Yaml emits []
350+
$yaml = [ordered]@{ items = @() } | ConvertTo-Yaml
351+
$yaml | Should -Match '\[\]'
352+
}
353+
354+
It 'Parses sequence items {} and [] correctly' {
355+
$yaml = @'
356+
- {}
357+
- []
358+
- hello
359+
'@
360+
$result = $yaml | ConvertFrom-Yaml -NoEnumerate -AsHashtable
361+
$result[0] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary]
362+
$result[0].Count | Should -Be 0
363+
$result[1].Count | Should -Be 0
364+
$result[2] | Should -Be 'hello'
365+
}
366+
}
332367
}

tests/ConvertTo-Yaml.Tests.ps1

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,69 @@ Describe 'ConvertTo-Yaml' {
178178
$yaml = $obj | ConvertTo-Yaml -Depth 5
179179
$yaml | Should -Match 'b: value'
180180
}
181+
182+
It 'Indents depth-exceeded values correctly under a mapping key' {
183+
$obj = [ordered]@{
184+
a = [ordered]@{
185+
b = [ordered]@{
186+
c = 'value'
187+
}
188+
}
189+
}
190+
$yaml = $obj | ConvertTo-Yaml -Depth 1 -WarningAction SilentlyContinue
191+
$lines = $yaml.TrimEnd("`r", "`n") -split "`r?`n"
192+
# The depth-exceeded line should be indented under 'b:'
193+
$bLine = $lines | Where-Object { $_ -match '^\s+b:' }
194+
$bLine | Should -Not -BeNullOrEmpty
195+
$depthLine = $lines[($lines.IndexOf($bLine) + 1)]
196+
$depthLine | Should -Match '^\s{4}'
197+
}
198+
}
199+
200+
Context 'Empty collections' {
201+
It 'Renders an empty hashtable as {}' {
202+
$yaml = [ordered]@{} | ConvertTo-Yaml
203+
$yaml.Trim() | Should -Be '{}'
204+
}
205+
206+
It 'Renders an empty array as []' {
207+
$yaml = ConvertTo-Yaml -InputObject @()
208+
$yaml.Trim() | Should -Be '[]'
209+
}
210+
211+
It 'Renders a nested empty mapping inline' {
212+
$obj = [ordered]@{ key = [ordered]@{} }
213+
$yaml = $obj | ConvertTo-Yaml
214+
$yaml.Trim() | Should -Be 'key: {}'
215+
}
216+
217+
It 'Renders a nested empty array inline' {
218+
$obj = [ordered]@{ key = @() }
219+
$yaml = $obj | ConvertTo-Yaml
220+
$yaml.Trim() | Should -Be 'key: []'
221+
}
222+
223+
It 'Renders an empty mapping value in a sequence-of-mappings inline' {
224+
$obj = @(
225+
[ordered]@{ name = 'Alice'; data = [ordered]@{} }
226+
)
227+
$yaml = ConvertTo-Yaml -InputObject $obj
228+
$yaml | Should -Match 'data: \{\}'
229+
}
230+
231+
It 'Renders an empty array value in a sequence-of-mappings inline' {
232+
$obj = @(
233+
[ordered]@{ name = 'Alice'; items = @() }
234+
)
235+
$yaml = ConvertTo-Yaml -InputObject $obj
236+
$yaml | Should -Match 'items: \[\]'
237+
}
238+
239+
It 'Renders an empty sequence item in a sequence as - []' {
240+
$obj = @( @(), @('a') )
241+
$yaml = ConvertTo-Yaml -InputObject $obj
242+
$yaml | Should -Match '(?m)^- \[\]'
243+
}
181244
}
182245

183246
Context 'Aliases' {
@@ -244,4 +307,17 @@ Describe 'Round-trip ConvertTo-Yaml | ConvertFrom-Yaml' {
244307
$result['c'] | Should -Be 'null'
245308
$result['c'] | Should -BeOfType [string]
246309
}
310+
311+
It 'Preserves an empty mapping under a key' {
312+
$obj = [ordered]@{ data = [ordered]@{} }
313+
$result = $obj | ConvertTo-Yaml | ConvertFrom-Yaml -AsHashtable
314+
$result['data'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary]
315+
$result['data'].Count | Should -Be 0
316+
}
317+
318+
It 'Preserves an empty array under a key' {
319+
$obj = [ordered]@{ items = @() }
320+
$result = $obj | ConvertTo-Yaml | ConvertFrom-Yaml -AsHashtable
321+
$result['items'].Count | Should -Be 0
322+
}
247323
}

0 commit comments

Comments
 (0)