From 509b5117a0abcf92d96f8fedde673615dcf6d21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Krzemi=C5=84ski?= <3110813+krzema12@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:26:11 +0100 Subject: [PATCH] [Port from snakeyaml-engine] Issue 54: Insert a trailing space when alias is a simple key (#309) Closes https://github.com/krzema12/snakeyaml-engine-kmp/issues/294 Ports: * https://bitbucket.org/snakeyaml/snakeyaml-engine/commits/1e74afe280795e6480ec72e7efee5afed44f2e25 * https://bitbucket.org/snakeyaml/snakeyaml-engine/commits/089376b4d2bbd7a0ee66808b7c1be8acc99feb04 --- .../snakeyaml/engine/kmp/emitter/Emitter.kt | 26 ++++++-- .../engine/kmp/scanner/ScannerImpl.kt | 12 ++-- .../issues/issue54/DumpWithoutSpaceTest.kt | 63 +++++++++++++++++++ .../engine/v2/emitter/EmitterTest.java | 17 ++--- 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/issues/issue54/DumpWithoutSpaceTest.kt diff --git a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/emitter/Emitter.kt b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/emitter/Emitter.kt index 4ed9cd4cc..b2ccd3c96 100644 --- a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/emitter/Emitter.kt +++ b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/emitter/Emitter.kt @@ -352,11 +352,11 @@ class Emitter( simpleKeyContext = simpleKey when (event?.eventId) { Event.ID.Alias -> { - expectAlias() + expectAlias(simpleKey) } Event.ID.Scalar, Event.ID.SequenceStart, Event.ID.MappingStart -> { - processAnchor("&") + processAnchor() processTag() handleNodeEvent(event!!.eventId) } @@ -396,9 +396,12 @@ class Emitter( } } - private fun expectAlias() { + /** + * @param simpleKey true when this is the alias for a simple key + */ + private fun expectAlias(simpleKey: Boolean) { state = if (event is AliasEvent) { - processAnchor("*") + processAlias(simpleKey) states.removeLast() } else { throw EmitterException("Expecting Alias.") @@ -782,7 +785,7 @@ class Emitter( //region Anchor, Tag, and Scalar processors. - private fun processAnchor(indicator: String) { + private fun processAnchorOrAlias(indicator: String, trailingWhitespace: Boolean) { val ev = event as NodeEvent val anchor: Anchor? = ev.anchor if (anchor != null) { @@ -792,6 +795,19 @@ class Emitter( writeIndicator(indicator = indicator + anchor, needWhitespace = true) } preparedAnchor = null + if (trailingWhitespace) { + writeWhitespace(1) + } + } + + private fun processAnchor() { + // no need for trailing space + processAnchorOrAlias("&", false) + } + + private fun processAlias(simpleKey: Boolean) { + // because of ':' it needs to add trailing space for simple keys + processAnchorOrAlias("*", simpleKey) } /** diff --git a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/scanner/ScannerImpl.kt b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/scanner/ScannerImpl.kt index e9b181385..f0ad7fa46 100644 --- a/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/scanner/ScannerImpl.kt +++ b/src/commonMain/kotlin/it/krzeminski/snakeyaml/engine/kmp/scanner/ScannerImpl.kt @@ -1279,11 +1279,10 @@ class ScannerImpl( /** * The YAML 1.2 specification does not restrict characters for anchors and - * aliases. This may lead to problems, see - * [issue 485](https://bitbucket.org/snakeyaml/snakeyaml/issues/485/alias-names-are-too-permissive-compared-to) + * aliases. This may lead to problems. * - * This implementation tries to follow - * [RFC-0003](https://github.com/yaml/yaml-spec/blob/master/rfc/RFC-0003.md) + * See [alias naming](https://github.com/yaml/libyaml/issues/205#issuecomment-693634465) + * See also [issue 485](https://bitbucket.org/snakeyaml/snakeyaml/issues/485/alias-names-are-too-permissive-compared-to) */ private fun scanAnchor(isAnchor: Boolean): Token { val startMark = reader.getMark() @@ -1292,7 +1291,10 @@ class ScannerImpl( reader.forward() var length = 0 var c = reader.peek(length) - // Anchor may not contain ",[]{}" + // The future implementation may try to follow RFC-0003: + // https://github.com/yaml/yaml-spec/blob/master/rfc/RFC-0003.md + // and exclude also ':' (colon) + // Anchor may not contain ,[]{}/.*& while (CharConstants.NULL_BL_T_LINEBR.hasNo(c, ",[]{}/.*&")) { length++ c = reader.peek(length) diff --git a/src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/issues/issue54/DumpWithoutSpaceTest.kt b/src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/issues/issue54/DumpWithoutSpaceTest.kt new file mode 100644 index 000000000..46fd0e5b5 --- /dev/null +++ b/src/commonTest/kotlin/it/krzeminski/snakeyaml/engine/kmp/issues/issue54/DumpWithoutSpaceTest.kt @@ -0,0 +1,63 @@ +package it.krzeminski.snakeyaml.engine.kmp.issues.issue54 + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import it.krzeminski.snakeyaml.engine.kmp.api.Dump +import it.krzeminski.snakeyaml.engine.kmp.api.DumpSettings +import it.krzeminski.snakeyaml.engine.kmp.api.Load +import it.krzeminski.snakeyaml.engine.kmp.api.LoadSettings + +/** + * Issue 54: add a space after anchor (when it is a simple key) + */ +class DumpWithoutSpaceTest : FunSpec({ + fun parse(data: String): Any? { + val loadSettings = LoadSettings.builder().setAllowRecursiveKeys(true).build(); + val load = Load(loadSettings); + return load.loadOne(data); + } + + test("The document does not have a space after the *1 alias") { + shouldThrow { + parse( + """|--- &1 + |hash: + | :one: true + | :two: true + | *1: true""".trimMargin() + ) + }.also { + it.message shouldContain "could not find expected ':'" + } + } + + test("The output does include a space after the *1 alias") { + val obj = parse( + """|--- &1 + |hash: + | :one: true + | :two: true + | *1 : true""".trimMargin() + ) + obj.shouldNotBeNull() + } + + test("Dump and load an alias") { + val map = mutableMapOf( + ":one" to true, + ) + map[map] = true + val dumpSettings = DumpSettings.builder().build() + val dump = Dump(dumpSettings) + val output = dump.dumpToString(map) + output shouldBe """|&id001 + |:one: true + |*id001 : true + |""".trimMargin() + val recursive = parse(output) + recursive.shouldNotBeNull() + } +}) diff --git a/src/jvmTest/java/org/snakeyaml/engine/v2/emitter/EmitterTest.java b/src/jvmTest/java/org/snakeyaml/engine/v2/emitter/EmitterTest.java index f7801a872..1cc829b21 100644 --- a/src/jvmTest/java/org/snakeyaml/engine/v2/emitter/EmitterTest.java +++ b/src/jvmTest/java/org/snakeyaml/engine/v2/emitter/EmitterTest.java @@ -256,7 +256,7 @@ public void testAnchorInMaps() { } @Test - @DisplayName("Expected space to separate anchor from colon") + @DisplayName("Expected space to separate alias from colon") public void testAliasAsKey() { DumpSettingsBuilder builder = DumpSettings.builder().setDefaultFlowStyle(FlowStyle.FLOW); // this is VERY BAD code @@ -266,20 +266,13 @@ public void testAliasAsKey() { f.put(f, "a"); String output = dump(builder.build(), f); - // TODO FIXME this YAML is invalid, the colon will be part of Anchor and not the separator - // key:value in the flow. - assertEquals("&id001 {*id001: a}\n", output); - Load load = new Load(); - try { - load.loadOne(output); - fail("TODO fix anchor"); - } catch (ComposerException e) { - assertTrue(e.getMessage().contains("found undefined alias id001:"), e.getMessage()); - } + assertEquals("&id001 {*id001 : a}\n", output); + Load load = new Load(LoadSettings.builder().setAllowRecursiveKeys(true).build()); + Object obj = load.loadOne(output); + assertNotNull(obj); } public static class MyDumperWriter extends StringWriter implements StreamDataWriter { - } @Test