Skip to content

Commit bf4dfbd

Browse files
Preserve cyclic YAML removal targets
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 1f5d117 commit bf4dfbd

3 files changed

Lines changed: 117 additions & 16 deletions

File tree

src/functions/private/Resolve-YamlRemovalTarget.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function Resolve-YamlRemovalTarget {
3636
Edge = $Document
3737
Node = $Document
3838
Depth = 0
39-
Ancestors = [string[]]::new(0)
39+
Path = [string[]] @($documentKey)
4040
Pointer = $Pointer
4141
DocumentIndex = $DocumentIndex
4242
}
@@ -122,7 +122,7 @@ function Resolve-YamlRemovalTarget {
122122
Edge = $matchedEntry
123123
Node = $matchedEntry.Value
124124
Depth = $Tokens.Count
125-
Ancestors = [string[]] $ancestors.ToArray()
125+
Path = [string[]] (@($ancestors.ToArray()) + $edgeKey)
126126
Pointer = $Pointer
127127
DocumentIndex = $DocumentIndex
128128
}
@@ -176,7 +176,7 @@ function Resolve-YamlRemovalTarget {
176176
Edge = $sequenceItem
177177
Node = $sequenceItem
178178
Depth = $Tokens.Count
179-
Ancestors = [string[]] $ancestors.ToArray()
179+
Path = [string[]] (@($ancestors.ToArray()) + $edgeKey)
180180
Pointer = $Pointer
181181
DocumentIndex = $DocumentIndex
182182
}

src/functions/private/Select-YamlRemovalTarget.ps1

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ function Select-YamlRemovalTarget {
2020
foreach ($target in $Targets) {
2121
Add-YamlRemovalWork -State $State -Operation 'target coalescing' -Node $target.Node
2222
if ($coalesced.ContainsKey($target.Key)) {
23-
$coalesced[$target.Key].AncestorSets.Add([string[]] $target.Ancestors)
23+
$coalesced[$target.Key].PathSets.Add([string[]] $target.Path)
2424
continue
2525
}
2626

27-
$ancestorSets = [System.Collections.Generic.List[object]]::new()
28-
$ancestorSets.Add([string[]] $target.Ancestors)
27+
$pathSets = [System.Collections.Generic.List[object]]::new()
28+
$pathSets.Add([string[]] $target.Path)
2929
$coalesced[$target.Key] = [pscustomobject]@{
3030
Key = $target.Key
3131
Kind = $target.Kind
@@ -34,28 +34,59 @@ function Select-YamlRemovalTarget {
3434
Edge = $target.Edge
3535
Node = $target.Node
3636
Depth = $target.Depth
37-
AncestorSets = $ancestorSets
37+
PathSets = $pathSets
3838
Pointer = $target.Pointer
3939
DocumentIndex = $target.DocumentIndex
4040
}
4141
}
4242

43-
$selectedKeys = [System.Collections.Generic.HashSet[string]]::new(
44-
[System.StringComparer]::Ordinal
45-
)
46-
foreach ($key in $coalesced.Keys) {
47-
[void] $selectedKeys.Add($key)
43+
$pathRoot = [pscustomobject]@{
44+
Children = [System.Collections.Generic.Dictionary[string, object]]::new(
45+
[System.StringComparer]::Ordinal
46+
)
47+
TargetKeys = [System.Collections.Generic.HashSet[string]]::new(
48+
[System.StringComparer]::Ordinal
49+
)
50+
}
51+
foreach ($target in $coalesced.Values) {
52+
foreach ($path in $target.PathSets) {
53+
$pathNode = $pathRoot
54+
foreach ($edgeKey in $path) {
55+
Add-YamlRemovalWork -State $State -Operation 'target path indexing' `
56+
-Node $target.Node
57+
if (-not $pathNode.Children.ContainsKey($edgeKey)) {
58+
$pathNode.Children[$edgeKey] = [pscustomobject]@{
59+
Children = [System.Collections.Generic.Dictionary[string, object]]::new(
60+
[System.StringComparer]::Ordinal
61+
)
62+
TargetKeys = [System.Collections.Generic.HashSet[string]]::new(
63+
[System.StringComparer]::Ordinal
64+
)
65+
}
66+
}
67+
$pathNode = $pathNode.Children[$edgeKey]
68+
}
69+
[void] $pathNode.TargetKeys.Add($target.Key)
70+
}
4871
}
4972

5073
$survivors = [System.Collections.Generic.List[object]]::new()
5174
foreach ($candidate in $coalesced.Values) {
5275
$allRequestsSubsumed = $true
53-
foreach ($ancestorSet in $candidate.AncestorSets) {
76+
foreach ($path in $candidate.PathSets) {
5477
$requestSubsumed = $false
55-
foreach ($ancestorKey in $ancestorSet) {
56-
Add-YamlRemovalWork -State $State -Operation 'ancestor coalescing' `
78+
$pathNode = $pathRoot
79+
for ($pathIndex = 0; $pathIndex -lt $path.Count - 1; $pathIndex++) {
80+
Add-YamlRemovalWork -State $State -Operation 'target path ancestry' `
5781
-Node $candidate.Node
58-
if ($selectedKeys.Contains($ancestorKey)) {
82+
$edgeKey = $path[$pathIndex]
83+
if (-not $pathNode.Children.ContainsKey($edgeKey)) {
84+
break
85+
}
86+
$pathNode = $pathNode.Children[$edgeKey]
87+
if ($pathNode.TargetKeys.Count -gt 1 -or
88+
($pathNode.TargetKeys.Count -eq 1 -and
89+
-not $pathNode.TargetKeys.Contains($candidate.Key))) {
5990
$requestSubsumed = $true
6091
break
6192
}

tests/Remove-YamlEntry.Tests.ps1

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,76 @@ node: &node
609609
$result['node'].Contains('self') | Should -BeFalse
610610
$result['node']['keep'] | Should -BeTrue
611611
}
612+
613+
It 'removes an edge that repeats in its own direct cyclic path' {
614+
$yaml = @'
615+
node: &node
616+
self: *node
617+
keep: true
618+
'@
619+
$result = Remove-YamlEntry $yaml '/node/self/self' |
620+
ConvertFrom-Yaml -AsHashtable
621+
622+
$result['node'].Contains('self') | Should -BeFalse
623+
$result['node']['keep'] | Should -BeTrue
624+
}
625+
626+
It 'removes an edge that repeats through a longer cycle' {
627+
$yaml = @'
628+
first: &first
629+
next: &second
630+
back: *first
631+
keep: true
632+
'@
633+
$result = Remove-YamlEntry $yaml '/first/next/back/next/back' |
634+
ConvertFrom-Yaml -AsHashtable
635+
636+
$result['first']['next'].Contains('back') | Should -BeFalse
637+
$result['first']['next']['keep'] | Should -BeTrue
638+
}
639+
640+
It 'does not mutually subsume targets reached through cyclic branches' {
641+
$yaml = @'
642+
node: &node
643+
a: *node
644+
b: *node
645+
keep: true
646+
'@
647+
$result = Remove-YamlEntry $yaml @('/node/a/b', '/node/b/a') |
648+
ConvertFrom-Yaml -AsHashtable
649+
650+
$result['node'].Contains('a') | Should -BeFalse
651+
$result['node'].Contains('b') | Should -BeFalse
652+
$result['node']['keep'] | Should -BeTrue
653+
}
654+
655+
It 'coalesces one cyclic target reached through duplicate aliases' {
656+
$yaml = @'
657+
node: &node
658+
self: *node
659+
keep: true
660+
copy: *node
661+
'@
662+
$result = Remove-YamlEntry $yaml @('/node/self/self', '/copy/self/self') |
663+
ConvertFrom-Yaml -AsHashtable
664+
665+
$result['node'].Contains('self') | Should -BeFalse
666+
[object]::ReferenceEquals($result['node'], $result['copy']) |
667+
Should -BeTrue
668+
}
669+
670+
It 'subsumes a cyclic descendant only under a proper requested ancestor' {
671+
$yaml = @'
672+
node: &node
673+
self: *node
674+
keep: true
675+
'@
676+
$result = Remove-YamlEntry $yaml @('/node/self', '/node/self/self/keep') |
677+
ConvertFrom-Yaml -AsHashtable
678+
679+
$result['node'].Contains('self') | Should -BeFalse
680+
$result['node']['keep'] | Should -BeTrue
681+
}
612682
}
613683

614684
Context 'Tags and resulting graph validation' {

0 commit comments

Comments
 (0)