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
11 changes: 11 additions & 0 deletions pkg/yaml/emitterc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,17 @@ func yaml_emitter_select_scalar_style(emitter *yaml_emitter_t, event *yaml_event
}

if style == yaml_PLAIN_SCALAR_STYLE {
// An untagged empty plain scalar resolves to null. It cannot be written
// as an empty plain scalar in flow context, and quoting it would emit
// the empty string instead, which is a different value. Write the null
// explicitly so the value survives. Block context is unaffected: an
// empty plain scalar is legal there and already round trips.
if emitter.flow_level > 0 && no_tag && event.implicit &&
len(emitter.scalar_data.value) == 0 {
if !yaml_emitter_analyze_scalar(emitter, []byte("null")) {
return false
}
}
if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed ||
emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed {
style = yaml_SINGLE_QUOTED_SCALAR_STYLE
Expand Down
51 changes: 51 additions & 0 deletions pkg/yaml/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2892,3 +2892,54 @@ func fprintCommentSet(out io.Writer, node *yaml.Node) {
fmt.Fprintf(out, "%q / %q / %q", node.HeadComment, node.LineComment, node.FootComment)
}
}

// TestNodeRoundtripPreservesData checks that decoding a document into a Node
// and encoding it again never changes the data the document holds. The emitter
// wrote an implicit null in a flow mapping as an empty single quoted scalar,
// which reads back as a string, so a round trip changed the value silently.
//
// The comparison decodes both the source and the emitted output into plain Go
// values, so it does not depend on the shape of any case and new cases can be
// added freely.
func (s *S) TestNodeRoundtripPreservesData(c *C) {
sources := []string{
// Implicit null in flow style, the regression this guards.
"a: {b: }\n",
"nested: {a: {b: }}\n",
"deep: {a: {b: {c: {d: }}}}\n",
"trailing: {a: 1, b: }\n",
"leading: {a: , b: 1}\n",
"every: {a: , b: , c: }\n",
// Flow nulls reached through sequences.
"seq: [{a: }, {b: 1}]\n",
"block seq:\n - {a: }\n - {b: }\n",
"both: {a: {b: }, c: [1, {d: }]}\n",
// Spellings that already survived, kept so they cannot regress.
"a: {b: null}\n",
"a: {b: ~}\n",
"a:\n b:\n",
// Empty strings must stay strings, not become null.
"a: {b: \"\"}\n",
"a: {b: ''}\n",
"a:\n b: \"\"\n",
"mixed: {a: , b: 1, c: \"\", d: null, e: ~, f: text}\n",
}

for _, source := range sources {
var node yaml.Node
err := yaml.Unmarshal([]byte(source), &node)
c.Assert(err, IsNil, Commentf("source %q", source))

out, err := yaml.Marshal(&node)
c.Assert(err, IsNil, Commentf("source %q", source))

var want, got interface{}
err = yaml.Unmarshal([]byte(source), &want)
c.Assert(err, IsNil, Commentf("source %q", source))
err = yaml.Unmarshal(out, &got)
c.Assert(err, IsNil, Commentf("source %q emitted %q", source, out))

c.Check(got, DeepEquals, want,
Commentf("source %q emitted %q", source, out))
}
}