From f44a4418e63871c130c642a026f18bc136e3d32a Mon Sep 17 00:00:00 2001 From: Thomas Manninger Date: Fri, 31 Jul 2026 10:46:53 +0200 Subject: [PATCH] fix nested syntax in custom options --- .../schema/internal/parser/SyntaxReader.kt | 9 +++- .../schema/internal/parser/ProtoParserTest.kt | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/internal/parser/SyntaxReader.kt b/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/internal/parser/SyntaxReader.kt index db6da39849..8a3e636640 100644 --- a/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/internal/parser/SyntaxReader.kt +++ b/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/internal/parser/SyntaxReader.kt @@ -199,7 +199,14 @@ class SyntaxReader( val start = pos loop@ while (pos < data.size) { when (data[pos]) { - in 'a'..'z', in 'A'..'Z', in '0'..'9', '_', '-', '.' -> pos++ + in 'a'..'z', in 'A'..'Z', in '0'..'9', '_', '-' -> pos++ + // A dot immediately followed by '(' or '[' is a separator before a parenthesized or + // bracketed extension (e.g. the second dot in "(foo.field).string.(foo.datetime)"), not + // part of this word. + '.' -> { + if (pos + 1 < data.size && (data[pos + 1] == '(' || data[pos + 1] == '[')) break@loop + pos++ + } else -> break@loop } } diff --git a/wire-schema/src/commonTest/kotlin/com/squareup/wire/schema/internal/parser/ProtoParserTest.kt b/wire-schema/src/commonTest/kotlin/com/squareup/wire/schema/internal/parser/ProtoParserTest.kt index f40835bb39..85dd895579 100644 --- a/wire-schema/src/commonTest/kotlin/com/squareup/wire/schema/internal/parser/ProtoParserTest.kt +++ b/wire-schema/src/commonTest/kotlin/com/squareup/wire/schema/internal/parser/ProtoParserTest.kt @@ -2549,6 +2549,54 @@ class ProtoParserTest { assertThat(ProtoParser.parse(location, proto)).isEqualTo(expected) } + // https://github.com/square/wire/issues/3672 + @Test + fun deepOptionAssignmentWithParenthesizedExtensionAfterFieldPathComponent() { + val proto = """ + |message Foo { + | optional string a = 1 [(foo.field).string.(foo.datetime) = true]; + |} + | + """.trimMargin() + val expected = ProtoFileElement( + location = location, + types = listOf( + MessageElement( + location = location.at(1, 1), + name = "Foo", + fields = listOf( + FieldElement( + location = location.at(2, 3), + label = OPTIONAL, + type = "string", + name = "a", + tag = 1, + options = listOf( + OptionElement( + name = "foo.field", + kind = Kind.OPTION, + isParenthesized = true, + value = OptionElement( + name = "string", + kind = Kind.OPTION, + isParenthesized = false, + value = OptionElement( + name = "foo.datetime", + kind = Kind.BOOLEAN, + isParenthesized = true, + value = "true", + ), + ), + ), + ), + ), + ), + ), + ), + ) + assertThat(ProtoParser.parse(location, proto)).isEqualTo(expected) + } + @Test fun protoKeywordAsEnumConstants() { // Note: this is consistent with protoc. val proto = """