Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions utils/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package utils

import (
"math/big"
"sort"
"strconv"
"strings"

"github.com/pb33f/go-yaml"
)

type enumValueIndex struct {
values map[string]int
nodes map[*yaml.Node]int
active map[*yaml.Node]bool
}

// CheckEnumForDuplicates returns each occurrence after the first of an equal
// JSON value, preserving input order and node identity. Numbers compare by value,
// arrays by ordered elements, and objects by members irrespective of key order.
// YAML aliases are followed; cyclic or non-JSON values are ignored.
func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node {
var duplicates []*yaml.Node
seen := make(map[int]bool)
index := enumValueIndex{
values: make(map[string]int),
nodes: make(map[*yaml.Node]int),
active: make(map[*yaml.Node]bool),
}
for _, node := range seq {
id := index.valueID(node)
if id == 0 {
continue
}
if seen[id] {
duplicates = append(duplicates, node)
}
seen[id] = true
}
return duplicates
}

// valueID interns each value using its children's IDs, keeping repeated aliases
// compact. Zero denotes a value outside the JSON data model.
func (index *enumValueIndex) valueID(node *yaml.Node) int {
if node == nil || index.active[node] {
return 0
}
if id, ok := index.nodes[node]; ok {
return id
}
index.active[node] = true
defer delete(index.active, node)
var id int
if node.Kind == yaml.AliasNode {
id = index.valueID(node.Alias)
} else if key, ok := index.valueKey(node); ok {
id = index.values[key]
if id == 0 {
id = len(index.values) + 1
index.values[key] = id
}
}
index.nodes[node] = id
return id
}

func (index *enumValueIndex) valueKey(node *yaml.Node) (string, bool) {
switch node.Kind {
case yaml.ScalarNode:
switch node.ShortTag() {
case "!!str":
return "s" + strconv.Quote(node.Value), true
case "!!null":
return "null", true
case "!!bool":
var value bool
if err := node.Decode(&value); err != nil {
return "", false
}
return strconv.FormatBool(value), true
case "!!int":
// YAML accepts base-prefixed integers and legacy octal (012 == 10).
value, ok := new(big.Int).SetString(node.Value, 0)
if !ok {
return "", false
}
return "n" + value.String(), true
case "!!float":
value, ok := new(big.Rat).SetString(node.Value)
if !ok {
return "", false
}
return "n" + value.RatString(), true
}
case yaml.SequenceNode:
if node.ShortTag() != "!!seq" {
return "", false
}
parts := make([]string, 0, len(node.Content))
for _, child := range node.Content {
id := index.valueID(child)
if id == 0 {
return "", false
}
parts = append(parts, strconv.Itoa(id))
}
return "[" + strings.Join(parts, ",") + "]", true
case yaml.MappingNode:
if node.ShortTag() != "!!map" || len(node.Content)%2 != 0 {
return "", false
}
parts := make([]string, 0, len(node.Content)/2)
keys := make(map[string]bool)
for i := 0; i < len(node.Content); i += 2 {
name := node.Content[i]
if name == nil || name.Kind != yaml.ScalarNode || name.ShortTag() != "!!str" || keys[name.Value] {
return "", false
}
keys[name.Value] = true
id := index.valueID(node.Content[i+1])
if id == 0 {
return "", false
}
parts = append(parts, strconv.Quote(name.Value)+":"+strconv.Itoa(id))
}
sort.Strings(parts)
return "{" + strings.Join(parts, ",") + "}", true
}
return "", false
}
87 changes: 87 additions & 0 deletions utils/enum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package utils

import (
"testing"

"github.com/pb33f/go-yaml"
"github.com/pb33f/testify/require"
)

func TestCheckEnumForDuplicates_Values(t *testing.T) {
for _, tc := range []struct {
name string
yaml string
duplicates []int
}{
{"distinct arrays", `[[a], [b], [a, b], [b, a], []]`, nil},
{"duplicate arrays", `[[a], [b], [a], [a]]`, []int{2, 3}},
{"distinct objects", `[{a: 1}, {a: 2}, {b: 1}, {}]`, nil},
{"object order", `[{a: 1, b: [2, 3]}, {b: [2.0, 3], a: 1.0}]`, []int{1}},
{"nested types", `[{a: [1]}, {a: ["1"]}]`, nil},
{"scalar types", `[1, "1", true, "true", null, "null", "", [], {}]`, nil},
{"octal integers", `[012, 10, 12, -012, -10, 0o12, 10.0]`, []int{1, 4, 5, 6}},
{"numbers", `[1, 1.0, 1e0, -0, 0.0, 0, 0.1, 1e-1]`, []int{1, 2, 4, 5, 7}},
{"precision", `[9007199254740992, 9007199254740993, 9007199254740993.0]`, []int{2}},
{"yaml scalars", `[true, TRUE, null, ~, 0x10, 16]`, []int{1, 3, 5}},
{"aliases", `[&a {x: [1]}, *a, {x: [1.0]}]`, []int{1, 2}},
{"repeated aliases", `[&a [1], [*a, *a], [[1], [1]]]`, []int{2}},
{"delimiters", `[["a,b", c], [a, "b,c"], {"a:b": c}, {a: "b:c"}]`, nil},
} {
t.Run(tc.name, func(t *testing.T) {
var root yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(tc.yaml), &root))
values := root.Content[0].Content
duplicates := CheckEnumForDuplicates(values)
require.Len(t, duplicates, len(tc.duplicates))
for i, index := range tc.duplicates {
require.Same(t, values[index], duplicates[i])
}
})
}
}

func TestCheckEnumForDuplicates_NonJSONValues(t *testing.T) {
for _, source := range []string{
`[&a [*a], &b [*b]]`,
`[{1: a}, {1: a}]`,
`[{a: 1, a: 2}, {a: 1, a: 2}]`,
`[.inf, .inf, .nan, .nan]`,
`[!custom a, !custom a]`,
`[!custom [], []]`,
`[!custom {}, {}]`,
`[{x: !custom a}, {x: !custom a}]`,
} {
t.Run(source, func(t *testing.T) {
var root yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(source), &root))
require.Empty(t, CheckEnumForDuplicates(root.Content[0].Content))
})
}
for _, node := range []*yaml.Node{
nil,
{Kind: yaml.AliasNode},
{Kind: yaml.MappingNode, Content: []*yaml.Node{nil}},
{Kind: yaml.MappingNode, Content: []*yaml.Node{nil, nil}},
{Kind: yaml.SequenceNode, Content: []*yaml.Node{nil}},
{Kind: yaml.ScalarNode, Tag: "!!bool", Value: "invalid"},
{Kind: yaml.ScalarNode, Tag: "!!int", Value: "invalid"},
{Kind: yaml.DocumentNode},
} {
require.Empty(t, CheckEnumForDuplicates([]*yaml.Node{node, node}))
}
}

func TestCheckEnumForDuplicates_SharedAliases(t *testing.T) {
value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: "1"}
// The graph is linear in size; expanding its aliases would create 2^128 leaves.
for range 128 {
value = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{
{Kind: yaml.AliasNode, Alias: value},
{Kind: yaml.AliasNode, Alias: value},
}}
}
alias := &yaml.Node{Kind: yaml.AliasNode, Alias: value}
duplicates := CheckEnumForDuplicates([]*yaml.Node{value, alias})
require.Len(t, duplicates, 1)
require.Same(t, alias, duplicates[0])
}
15 changes: 0 additions & 15 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1218,21 +1218,6 @@ func DetectCase(input string) Case {
return RegularCase
}

// CheckEnumForDuplicates will check an array of nodes to check if there are any duplicate values.
func CheckEnumForDuplicates(seq []*yaml.Node) []*yaml.Node {
var res []*yaml.Node
seen := make(map[string]*yaml.Node)

for _, enum := range seq {
if seen[enum.Value] != nil {
res = append(res, enum)
continue
}
seen[enum.Value] = enum
}
return res
}

// DetermineWhitespaceLengthBytes determines the minimum leading-space indentation
// in the input, working directly on []byte without allocating strings or regex matches.
// Matches the semantics of the regex `\n( +)`: only considers lines after a newline.
Expand Down
Loading