Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ class OptionReader(internal val reader: SyntaxReader) {
break
}
// Read nested field name. For example "baz" in "(foo.bar).baz = 12".
val subName = reader.readName(retainWrap = true)
if (subName.startsWith("(")) {
subNames.add(subName)
} else {
subNames.addAll(subName.split("."))
}
//
// The dots in an option name are separators, so a component must not absorb the ones that
// follow it: in "(foo.field).string.(foo.datetime)" this reads "string" and leaves the next
// dot for the loop above. Consuming dots here rather than inside the word is also what makes
// whitespace and comments around them insignificant, as they are everywhere else.
subNames += reader.readName(retainWrap = true, allowDots = false)
}
if (keyValueSeparator == ':' && c == '{') {
// In text format, values which are maps can omit a separator. Backtrack so it can be re-read.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,15 @@ class SyntaxReader(
* If {@code retainWrap} is true and the symbol was wrapped in parens
* or square brackets, the returned string retains the wrapping
* punctuation. Otherwise, just the symbol is returned.
*
* @param allowDots whether a naked name may span '.' characters. Wrapped names always may; they
* are delimited by their wrapping punctuation rather than by dots.
*/
fun readName(allowLeadingDigit: Boolean = true, retainWrap: Boolean = false): String = when (peekChar()) {
fun readName(
allowLeadingDigit: Boolean = true,
retainWrap: Boolean = false,
allowDots: Boolean = true,
): String = when (peekChar()) {
'(' -> {
pos++
val word = readWord(allowLeadingDigit).also {
Expand All @@ -160,7 +167,7 @@ class SyntaxReader(
if (retainWrap) "[$word]" else word
}

else -> readWord(allowLeadingDigit)
else -> readWord(allowLeadingDigit, allowDots)
}

/** Reads a scalar, map, or type name. */
Expand Down Expand Up @@ -193,13 +200,20 @@ class SyntaxReader(
}
}

/** Reads a non-empty word and returns it. */
fun readWord(allowLeadingDigit: Boolean = true): String {
/**
* Reads a non-empty word and returns it.
*
* @param allowDots true for the word to span '.' characters, as in the qualified name `foo.Bar`.
* When false the word ends at the first '.', leaving it for the caller to consume as a
* separator between the components of a path.
*/
fun readWord(allowLeadingDigit: Boolean = true, allowDots: Boolean = true): String {
skipWhitespace(skipComments = true)
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++
'.' -> if (allowDots) pos++ else break@loop
else -> break@loop
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2549,6 +2549,79 @@ 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)
}

// https://github.com/square/wire/issues/3672
@Test
fun deepOptionAssignmentDotsAreSeparators() {
// The dots between the components of an option name are separators, so whitespace and comments
// around them are insignificant, as they are between any other two tokens.
val optionNames = listOf(
"(foo.field).string.(foo.datetime)",
"(foo.field).string. (foo.datetime)",
"(foo.field).string . (foo.datetime)",
"(foo.field) . string . (foo.datetime)",
"(foo.field).string./* comment */(foo.datetime)",
"(foo.field).string.\n (foo.datetime)",
)
val canonical = parseOptionOnFieldA(optionNames.first())
for (optionName in optionNames) {
assertThat(parseOptionOnFieldA(optionName), name = optionName).isEqualTo(canonical)
}
}

/** Parses a message whose only field carries `optionName` set to `true`. */
private fun parseOptionOnFieldA(optionName: String): ProtoFileElement = ProtoParser.parse(
location,
"message Foo {\n optional string a = 1 [$optionName = true];\n}\n",
)

@Test fun protoKeywordAsEnumConstants() {
// Note: this is consistent with protoc.
val proto = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,45 @@ class OptionsTest {
.isEqualTo(mapOf(fooOptions to mapOf(opt1 to "456", opt2 to "quux")))
}

// https://github.com/square/wire/issues/3672
@Test
fun parenthesizedExtensionAfterFieldPathComponent() {
// Shaped after protovalidate's "(buf.validate.field).string.(buf.validate.predefined)".
val schema = buildSchema {
add(
"foo.proto".toPath(),
"""
|import "google/protobuf/descriptor.proto";
|message FieldConstraints {
| optional StringRules string = 1;
|}
|message StringRules {
| extensions 1000 to max;
|}
|
|extend StringRules {
| optional bool datetime = 1000;
|}
|extend google.protobuf.FieldOptions {
| optional FieldConstraints field = 1234;
|}
|
|message Bar {
| optional string a = 1 [(field).string.(datetime) = true];
|}
""".trimMargin(),
)
}

val field = ProtoMember.get(Options.FIELD_OPTIONS, "field")
val string = ProtoMember.get(ProtoType.get("FieldConstraints"), "string")
val datetime = ProtoMember.get(ProtoType.get("StringRules"), "datetime")

val bar = schema.getType("Bar") as MessageType
assertThat(bar.field("a")!!.options.map)
.isEqualTo(mapOf(field to mapOf(string to mapOf(datetime to "true"))))
}

@Test
fun textFormatCanOmitMapValueSeparator() {
val schema = buildSchema {
Expand Down