From 36ab947f989130abfc80a87568bc5a5ac7d4cedc Mon Sep 17 00:00:00 2001 From: Egor Andreevici Date: Fri, 10 Nov 2017 08:57:01 +0200 Subject: [PATCH 1/3] Smarter wrapping logic for long parameter lists --- .../com/squareup/kotlinpoet/CodeWriter.kt | 6 + .../java/com/squareup/kotlinpoet/FunSpec.kt | 2 +- .../com/squareup/kotlinpoet/LineWrapper.kt | 152 +++++++++++++++--- .../com/squareup/kotlinpoet/ParameterSpec.kt | 25 +-- .../java/com/squareup/kotlinpoet/TypeSpec.kt | 2 +- .../com/squareup/kotlinpoet/KotlinPoetTest.kt | 59 +++++-- .../com/squareup/kotlinpoet/TypeSpecTest.kt | 24 +-- 7 files changed, 200 insertions(+), 70 deletions(-) diff --git a/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt b/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt index 2dd6f70750..07a773e261 100644 --- a/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt +++ b/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt @@ -275,6 +275,12 @@ internal class CodeWriter constructor( } } + fun openWrappingGroup() = out.openWrappingGroup() + + fun closeWrappingGroup() { + trailingNewline = out.closeWrappingGroup() + } + fun emitWrappingSpace() = apply { out.wrappingSpace(indentLevel + 2) } diff --git a/src/main/java/com/squareup/kotlinpoet/FunSpec.kt b/src/main/java/com/squareup/kotlinpoet/FunSpec.kt index 79b470473d..9c97385214 100644 --- a/src/main/java/com/squareup/kotlinpoet/FunSpec.kt +++ b/src/main/java/com/squareup/kotlinpoet/FunSpec.kt @@ -115,7 +115,7 @@ class FunSpec private constructor(builder: Builder) { codeWriter.emitCode("%L", escapeIfKeyword(name)) } - parameters.emit(codeWriter) { param -> + parameters.emit(codeWriter, wrappable = true) { param -> param.emit(codeWriter, includeType = name != SETTER) } diff --git a/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt b/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt index 7e098594be..b0ca08590e 100644 --- a/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt +++ b/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt @@ -26,71 +26,185 @@ internal class LineWrapper( ) { private var closed = false - /** Characters written since the last wrapping space that haven't yet been flushed. */ - private val buffer = StringBuilder() - /** The number of characters since the most recent newline. Includes both out and the buffer. */ private var column = 0 - /** -1 if we have no buffering; otherwise the number of spaces to write after wrapping. */ - private var indentLevel = -1 + private var helper: BufferedLineWrapperHelper = DefaultLineWrapperHelper() /** Emit `s`. This may be buffered to permit line wraps to be inserted. */ fun append(s: String) { check(!closed) { "closed" } - if (indentLevel != -1) { + if (helper.isBuffering) { val nextNewline = s.indexOf('\n') // If s doesn't cause the current line to cross the limit, buffer it and return. We'll decide // whether or not we have to wrap it later. if (nextNewline == -1 && column + s.length <= columnLimit) { - buffer.append(s) + helper.buffer(s) column += s.length return } // Wrap if appending s would overflow the current line. val wrap = nextNewline == -1 || column + nextNewline > columnLimit - flush(wrap) + helper.flush(wrap) } - out.append(s) + helper.append(s) val lastNewline = s.lastIndexOf('\n') column = if (lastNewline != -1) s.length - lastNewline - 1 else column + s.length } + fun openWrappingGroup() { + check(!closed) { "closed" } + + helper = GroupLineWrapperHelper() + } + /** Emit either a space or a newline character. */ fun wrappingSpace(indentLevel: Int) { check(!closed) { "closed" } - if (this.indentLevel != -1) flush(false) + helper.wrappingSpace(indentLevel) this.column++ - this.indentLevel = indentLevel + } + + fun closeWrappingGroup(): Boolean { + check(!closed) { "closed" } + + val wrapped = helper.close() + helper = DefaultLineWrapperHelper() + return wrapped } /** Flush any outstanding text and forbid future writes to this line wrapper. */ fun close() { - if (indentLevel != -1) flush(false) + helper.close() closed = true } /** Write the space followed by any buffered text that follows it. */ - private fun flush(wrap: Boolean) { + private fun flush(buffered: String, wrap: Boolean) { if (wrap) { out.append('\n') - for (i in 0 until indentLevel) { + for (i in 0 until helper.indentLevel) { out.append(indent) } - column = indentLevel * indent.length - column += buffer.length + column = helper.indentLevel * indent.length + column += buffered.length } else { out.append(' ') } - out.append(buffer) - buffer.delete(0, buffer.length) - indentLevel = -1 + out.append(buffered) + } + + /** + * Contract for helpers that handle buffering, post-processing and flushing of the input. + */ + internal interface BufferedLineWrapperHelper { + + val indentLevel: Int + + val isBuffering get() = indentLevel != -1 + + /** Append to out, bypassing the buffer */ + fun append(s: String): Appendable + + /** Append to buffer */ + fun buffer(s: String): Appendable + + /** + * Indicates that a new wrapping space occurred in input. + * + * @param indentLevel Indentation level for the new line + */ + fun wrappingSpace(indentLevel: Int) + + /** + * Flush any buffered text. + * + * @param wrap `true` if buffer contents should be flushed a on new line + * */ + fun flush(wrap: Boolean) + + /** + * Flush and clear the buffer. + * + * @return `true` if input wrapped to new line + */ + fun close(): Boolean + } + + /** Flushes the buffer each time the wrapping space is encountered */ + internal inner class DefaultLineWrapperHelper : BufferedLineWrapperHelper { + + private val buffer = StringBuilder() + + private var _indentLevel = -1 + + override val indentLevel get() = _indentLevel + + override fun append(s: String): Appendable = out.append(s) + + override fun buffer(s: String): Appendable = buffer.append(s) + + override fun wrappingSpace(indentLevel: Int) { + if (isBuffering) flush(false) + _indentLevel = indentLevel + } + + override fun flush(wrap: Boolean) { + flush(buffer.toString(), wrap) + buffer.delete(0, buffer.length) + _indentLevel = -1 + } + + override fun close(): Boolean { + if (isBuffering) flush(false) + return false + } + } + + /** + * Holds multiple buffers and only flushes when the group is closed. If wrapping happened within + * a group - each buffer will be flushed on a new line. + */ + internal inner class GroupLineWrapperHelper : BufferedLineWrapperHelper { + + private val buffer = mutableListOf(StringBuilder()) + private var wrapped = false + + private var _indentLevel = -1 + + override val indentLevel get() = _indentLevel + + override fun append(s: String): Appendable = buffer.last().append(s) + + override fun buffer(s: String): Appendable = buffer.last().append(s) + + override fun wrappingSpace(indentLevel: Int) { + _indentLevel = indentLevel + buffer += StringBuilder() + } + + override fun flush(wrap: Boolean) { + wrapped = wrap + } + + override fun close(): Boolean { + if (wrapped) buffer.last().append('\n') + buffer.forEachIndexed { index, segment -> + if (index == 0 && !wrapped) { + out.append(segment) + } else { + flush(segment.toString(), wrapped) + } + } + _indentLevel = -1 + return wrapped + } } } diff --git a/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt b/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt index 23d133a44f..837a0e4a02 100644 --- a/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt +++ b/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt @@ -145,28 +145,15 @@ class ParameterSpec private constructor(builder: ParameterSpec.Builder) { internal fun List.emit( codeWriter: CodeWriter, + wrappable: Boolean = false, emitBlock: (ParameterSpec) -> Unit = { it.emit(codeWriter) } ) = with(codeWriter) { - val params = this@emit emit("(") - when (size) { - 0 -> emit("") - 1 -> emitBlock(params[0]) - 2 -> { - emitBlock(params[0]) - emit(", ") - emitBlock(params[1]) - } - else -> { - emit("\n") - indent(2) - forEachIndexed { index, parameter -> - if (index > 0) emit(",\n") - emitBlock(parameter) - } - unindent(2) - emit("\n") - } + if (wrappable) codeWriter.openWrappingGroup() + forEachIndexed { index, parameter -> + if (index > 0) if (wrappable) emitCode(",%W") else emit(", ") + emitBlock(parameter) } + if (wrappable) codeWriter.closeWrappingGroup() emit(")") } diff --git a/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt b/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt index c443c141bd..ba614dc4f4 100644 --- a/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt +++ b/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt @@ -115,7 +115,7 @@ class TypeSpec private constructor(builder: TypeSpec.Builder) { codeWriter.emit("constructor") } - it.parameters.emit(codeWriter) { param -> + it.parameters.emit(codeWriter, wrappable = true) { param -> val property = constructorProperties[param.name] if (property != null) { property.emit(codeWriter, setOf(PUBLIC), withInitializer = false, inline = true) diff --git a/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt b/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt index 7f71614f78..f14a674f68 100644 --- a/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt +++ b/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt @@ -17,6 +17,7 @@ package com.squareup.kotlinpoet import com.google.common.truth.Truth.assertThat import org.junit.Test +import java.io.Serializable class KotlinPoetTest { private val tacosPackage = "com.squareup.tacos" @@ -107,11 +108,7 @@ class KotlinPoetTest { |import kotlin.Boolean |import kotlin.String | - |class Taco( - | val cheese: String, - | var cilantro: String, - | lettuce: String - |) { + |class Taco(val cheese: String, var cilantro: String, lettuce: String) { | val lettuce: String = lettuce.trim() | | val onion: Boolean = true @@ -365,11 +362,7 @@ class KotlinPoetTest { |import kotlin.String |import kotlin.Unit | - |fun (( - | name: String, - | Int, - | age: Long - |) -> Unit).whatever(): Unit = Unit + |fun ((name: String, Int, age: Long) -> Unit).whatever(): Unit = Unit |""".trimMargin()) } @@ -580,4 +573,50 @@ class KotlinPoetTest { |} |""".trimMargin()) } + + @Test fun longParameterListWrapping() { + val source = FunSpec.builder("sum") + .addParameter(ParameterSpec.builder("a", Int::class).build()) + .addParameter(ParameterSpec.builder("b", Int::class).build()) + .addParameter(ParameterSpec.builder("c", Int::class).build()) + .addParameter(ParameterSpec.builder("d", Int::class).build()) + .addParameter(ParameterSpec.builder("e", Int::class).build()) + .addParameter(ParameterSpec.builder("f", Int::class).build()) + .addParameter(ParameterSpec.builder("g", Int::class).build()) + .addStatement("return a + b + c") + .build() + assertThat(source.toString()).isEqualTo(""" + |fun sum( + | a: kotlin.Int, + | b: kotlin.Int, + | c: kotlin.Int, + | d: kotlin.Int, + | e: kotlin.Int, + | f: kotlin.Int, + | g: kotlin.Int + |) = a + b + c + |""".trimMargin()) + } + + @Test fun longLambdaParameterListWrapping() { + val source = FunSpec.builder("veryLongFunctionName") + .addParameter(ParameterSpec.builder( + "veryLongParameterName", + LambdaTypeName.get( + parameters = listOf( + ParameterSpec.unnamed(Serializable::class), + ParameterSpec.unnamed(Appendable::class), + ParameterSpec.unnamed(Cloneable::class)), + returnType = Unit::class.asTypeName())) + .build()) + .addParameter("i", Int::class) + .addStatement("return %T", Unit::class) + .build() + assertThat(source.toString()).isEqualTo(""" + |fun veryLongFunctionName( + | veryLongParameterName: (java.io.Serializable, java.lang.Appendable, kotlin.Cloneable) -> kotlin.Unit, + | i: kotlin.Int + |) = kotlin.Unit + |""".trimMargin()) + } } diff --git a/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt b/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt index a0e1c85114..50325bb725 100644 --- a/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt +++ b/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt @@ -579,11 +579,7 @@ class TypeSpecTest { | | override fun compareTo(p: P): Int = 0 | - | fun of( - | label: T, - | x: P, - | y: P - | ): Location { + | fun of(label: T, x: P, y: P): Location { | throw new UnsupportedOperationException("TODO") | } |} @@ -1100,11 +1096,7 @@ class TypeSpecTest { |import kotlin.Int | |class Taqueria { - | fun prepare( - | workers: Int, - | vararg jobs: Runnable, - | start: Boolean - | ) { + | fun prepare(workers: Int, vararg jobs: Runnable, start: Boolean) { | } |} |""".trimMargin()) @@ -2580,11 +2572,7 @@ class TypeSpecTest { |import kotlin.String |import kotlin.collections.Map | - |class Taco( - | val a: String?, - | val b: String?, - | val c: String? - |) { + |class Taco(val a: String?, val b: String?, val c: String?) { | constructor(map: Map) : this(map["a"], map["b"], map["c"]) |} |""".trimMargin()) @@ -2669,11 +2657,7 @@ class TypeSpecTest { |import kotlin.Int |import kotlin.String | - |data class Person( - | override val id: Int, - | override val name: String, - | override val surname: String - |) + |data class Person(override val id: Int, override val name: String, override val surname: String) |""".trimMargin()) } From 1f6b2924409bcae0bbc4b38559acdcc0c3d5f5e0 Mon Sep 17 00:00:00 2001 From: Egor Andreevici Date: Sat, 16 Dec 2017 01:15:02 +0200 Subject: [PATCH 2/3] Opt-out wrapping --- src/main/java/com/squareup/kotlinpoet/FunSpec.kt | 2 +- src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt | 2 +- src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt | 8 ++++---- src/main/java/com/squareup/kotlinpoet/TypeSpec.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/squareup/kotlinpoet/FunSpec.kt b/src/main/java/com/squareup/kotlinpoet/FunSpec.kt index 9c97385214..79b470473d 100644 --- a/src/main/java/com/squareup/kotlinpoet/FunSpec.kt +++ b/src/main/java/com/squareup/kotlinpoet/FunSpec.kt @@ -115,7 +115,7 @@ class FunSpec private constructor(builder: Builder) { codeWriter.emitCode("%L", escapeIfKeyword(name)) } - parameters.emit(codeWriter, wrappable = true) { param -> + parameters.emit(codeWriter) { param -> param.emit(codeWriter, includeType = name != SETTER) } diff --git a/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt b/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt index 229bda68cd..2613921e6d 100644 --- a/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt +++ b/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt @@ -65,7 +65,7 @@ class LambdaTypeName internal constructor( out.emitCode("%T.", it) } - parameters.emit(out) + parameters.emit(out, wrap = false) out.emitCode(" -> %T", returnType) if (nullable) { diff --git a/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt b/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt index 837a0e4a02..97ab52e8ca 100644 --- a/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt +++ b/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt @@ -145,15 +145,15 @@ class ParameterSpec private constructor(builder: ParameterSpec.Builder) { internal fun List.emit( codeWriter: CodeWriter, - wrappable: Boolean = false, + wrap: Boolean = true, emitBlock: (ParameterSpec) -> Unit = { it.emit(codeWriter) } ) = with(codeWriter) { emit("(") - if (wrappable) codeWriter.openWrappingGroup() + if (wrap) codeWriter.openWrappingGroup() forEachIndexed { index, parameter -> - if (index > 0) if (wrappable) emitCode(",%W") else emit(", ") + if (index > 0) if (wrap) emitCode(",%W") else emit(", ") emitBlock(parameter) } - if (wrappable) codeWriter.closeWrappingGroup() + if (wrap) codeWriter.closeWrappingGroup() emit(")") } diff --git a/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt b/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt index ba614dc4f4..c443c141bd 100644 --- a/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt +++ b/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt @@ -115,7 +115,7 @@ class TypeSpec private constructor(builder: TypeSpec.Builder) { codeWriter.emit("constructor") } - it.parameters.emit(codeWriter, wrappable = true) { param -> + it.parameters.emit(codeWriter) { param -> val property = constructorProperties[param.name] if (property != null) { property.emit(codeWriter, setOf(PUBLIC), withInitializer = false, inline = true) From 09a1c92f2cf72ac444f40ab5dbaafa122f8b4678 Mon Sep 17 00:00:00 2001 From: Egor Andreevici Date: Sat, 23 Dec 2017 19:01:07 +0200 Subject: [PATCH 3/3] Use default indent in parameter lists --- .../com/squareup/kotlinpoet/CodeWriter.kt | 14 +++- .../com/squareup/kotlinpoet/KotlinPoetTest.kt | 18 ++--- .../com/squareup/kotlinpoet/TypeSpecTest.kt | 80 +++++++++---------- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt b/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt index 07a773e261..e9ea182afa 100644 --- a/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt +++ b/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt @@ -18,6 +18,9 @@ package com.squareup.kotlinpoet /** Sentinel value that indicates that no user-provided package has been set. */ private val NO_PACKAGE = String() +private const val DEFAULT_INDENT_LEVEL = 1 +private const val CONTINUATION_INDENT_LEVEL = 2 + private fun extractMemberName(part: String): String { require(Character.isJavaIdentifierStart(part[0])) { "not an identifier: $part" } for (i in 1..part.length) { @@ -40,6 +43,7 @@ internal class CodeWriter constructor( ) { private val out = LineWrapper(out, indent, 100) private var indentLevel = 0 + private var indentLevelIncrement = CONTINUATION_INDENT_LEVEL private var kdoc = false private var comment = false @@ -249,7 +253,7 @@ internal class CodeWriter constructor( statementLine = -1 } - "%W" -> out.wrappingSpace(indentLevel + 2) + "%W" -> out.wrappingSpace(indentLevel + indentLevelIncrement) else -> { // Handle deferred type. @@ -275,14 +279,18 @@ internal class CodeWriter constructor( } } - fun openWrappingGroup() = out.openWrappingGroup() + fun openWrappingGroup() { + out.openWrappingGroup() + indentLevelIncrement = DEFAULT_INDENT_LEVEL + } fun closeWrappingGroup() { trailingNewline = out.closeWrappingGroup() + indentLevelIncrement = CONTINUATION_INDENT_LEVEL } fun emitWrappingSpace() = apply { - out.wrappingSpace(indentLevel + 2) + out.wrappingSpace(indentLevel + indentLevelIncrement) } private fun emitStaticImportMember(canonical: String, part: String): Boolean { diff --git a/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt b/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt index f14a674f68..ae3d0955b7 100644 --- a/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt +++ b/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt @@ -587,13 +587,13 @@ class KotlinPoetTest { .build() assertThat(source.toString()).isEqualTo(""" |fun sum( - | a: kotlin.Int, - | b: kotlin.Int, - | c: kotlin.Int, - | d: kotlin.Int, - | e: kotlin.Int, - | f: kotlin.Int, - | g: kotlin.Int + | a: kotlin.Int, + | b: kotlin.Int, + | c: kotlin.Int, + | d: kotlin.Int, + | e: kotlin.Int, + | f: kotlin.Int, + | g: kotlin.Int |) = a + b + c |""".trimMargin()) } @@ -614,8 +614,8 @@ class KotlinPoetTest { .build() assertThat(source.toString()).isEqualTo(""" |fun veryLongFunctionName( - | veryLongParameterName: (java.io.Serializable, java.lang.Appendable, kotlin.Cloneable) -> kotlin.Unit, - | i: kotlin.Int + | veryLongParameterName: (java.io.Serializable, java.lang.Appendable, kotlin.Cloneable) -> kotlin.Unit, + | i: kotlin.Int |) = kotlin.Unit |""".trimMargin()) } diff --git a/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt b/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt index 50325bb725..60162cd83d 100644 --- a/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt +++ b/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt @@ -185,11 +185,11 @@ class TypeSpecTest { | |class Foo { | constructor( - | id: Long, - | @Ping one: String, - | @Ping two: String, - | @Pong("pong") three: String, - | @Ping four: String + | id: Long, + | @Ping one: String, + | @Ping two: String, + | @Pong("pong") three: String, + | @Ping four: String | ) { | /* code snippets */ | } @@ -272,9 +272,9 @@ class TypeSpecTest { | ) | @POST("/foo/bar") | fun fooBar( - | @Body things: Things, - | @QueryMap(encodeValues = false) query: Map, - | @Header("Authorization") authorization: String + | @Body things: Things, + | @QueryMap(encodeValues = false) query: Map, + | @Header("Authorization") authorization: String | ): Observable |} |""".trimMargin()) @@ -2056,38 +2056,38 @@ class TypeSpecTest { | |class Taco { | fun call( - | s0: String, - | s1: String, - | s2: String, - | s3: String, - | s4: String, - | s5: String, - | s6: String, - | s7: String, - | s8: String, - | s9: String, - | s10: String, - | s11: String, - | s12: String, - | s13: String, - | s14: String, - | s15: String, - | s16: String, - | s17: String, - | s18: String, - | s19: String, - | s20: String, - | s21: String, - | s22: String, - | s23: String, - | s24: String, - | s25: String, - | s26: String, - | s27: String, - | s28: String, - | s29: String, - | s30: String, - | s31: String + | s0: String, + | s1: String, + | s2: String, + | s3: String, + | s4: String, + | s5: String, + | s6: String, + | s7: String, + | s8: String, + | s9: String, + | s10: String, + | s11: String, + | s12: String, + | s13: String, + | s14: String, + | s15: String, + | s16: String, + | s17: String, + | s18: String, + | s19: String, + | s20: String, + | s21: String, + | s22: String, + | s23: String, + | s24: String, + | s25: String, + | s26: String, + | s27: String, + | s28: String, + | s29: String, + | s30: String, + | s31: String | ) { | call("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", | "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29",