From b3ceb0f625a5b40d1007bb4dfe673be497fc2c9e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Feb 2022 14:57:37 +0100 Subject: [PATCH] Add core JavaPoet utilities This commit adds utilities that facilitate code generation patterns used by the AOT engine. Closes gh-28028 --- .../javapoet/support/CodeSnippet.java | 188 +++++++++++++++++ .../javapoet/support/MultiCodeBlock.java | 82 ++++++++ .../javapoet/support/MultiStatement.java | 194 ++++++++++++++++++ .../javapoet/support/package-info.java | 9 + .../javapoet/support/CodeSnippetTests.java | 75 +++++++ .../javapoet/support/MultiCodeBlockTests.java | 61 ++++++ .../javapoet/support/MultiStatementTests.java | 131 ++++++++++++ 7 files changed, 740 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/javapoet/support/CodeSnippet.java create mode 100644 spring-core/src/main/java/org/springframework/javapoet/support/MultiCodeBlock.java create mode 100644 spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java create mode 100644 spring-core/src/main/java/org/springframework/javapoet/support/package-info.java create mode 100644 spring-core/src/test/java/org/springframework/javapoet/support/CodeSnippetTests.java create mode 100644 spring-core/src/test/java/org/springframework/javapoet/support/MultiCodeBlockTests.java create mode 100644 spring-core/src/test/java/org/springframework/javapoet/support/MultiStatementTests.java diff --git a/spring-core/src/main/java/org/springframework/javapoet/support/CodeSnippet.java b/spring-core/src/main/java/org/springframework/javapoet/support/CodeSnippet.java new file mode 100644 index 00000000000..ba2f47815cb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/javapoet/support/CodeSnippet.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.javapoet.support; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.lang.model.element.Modifier; + +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.TypeSpec; + +/** + * A code snippet using tabs indentation that is fully processed by JavaPoet so + * that imports are resolved. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class CodeSnippet { + + private static final String START_SNIPPET = "// start-snippet\n"; + + private static final String END_SNIPPET = "// end-snippet"; + + private final String fileContent; + + private final String snippet; + + + CodeSnippet(String fileContent, String snippet) { + this.fileContent = fileContent; + this.snippet = snippet; + } + + + String getFileContent() { + return this.fileContent; + } + + /** + * Return the rendered code snippet. + * @return a code snippet where imports have been resolved + */ + public String getSnippet() { + return this.snippet; + } + + /** + * Specify if an import statement for the specified type is present. + * @param type the type to check + * @return true if this type has an import statement, false otherwise + */ + public boolean hasImport(Class type) { + return hasImport(type.getName()); + } + + /** + * Specify if an import statement for the specified class name is present. + * @param className the name of the class to check + * @return true if this type has an import statement, false otherwise + */ + public boolean hasImport(String className) { + return getFileContent().lines().anyMatch(candidate -> + candidate.equals(String.format("import %s;", className))); + } + + /** + * Return a new {@link CodeSnippet} where the specified number of indentations + * have been removed. + * @param indent the number of indent to remove + * @return a CodeSnippet instance with the number of indentations removed + */ + public CodeSnippet removeIndent(int indent) { + return new CodeSnippet(this.fileContent, this.snippet.lines().map(line -> + removeIndent(line, indent)).collect(Collectors.joining("\n"))); + } + + /** + * Create a {@link CodeSnippet} using the specified code. + * @param code the code snippet + * @return a {@link CodeSnippet} instance + */ + public static CodeSnippet of(CodeBlock code) { + return new Builder().build(code); + } + + /** + * Process the specified code and return a fully-processed code snippet + * as a String. + * @param code a consumer to use to generate the code snippet + * @return a resolved code snippet + */ + public static String process(Consumer code) { + CodeBlock.Builder body = CodeBlock.builder(); + code.accept(body); + return process(body.build()); + } + + /** + * Process the specified {@link CodeBlock code} and return a + * fully-processed code snippet as a String. + * @param code the code snippet + * @return a resolved code snippet + */ + public static String process(CodeBlock code) { + return of(code).getSnippet(); + } + + private String removeIndent(String line, int indent) { + for (int i = 0; i < indent; i++) { + if (line.startsWith("\t")) { + line = line.substring(1); + } + } + return line; + } + + private static final class Builder { + + private static final String INDENT = "\t"; + + private static final String SNIPPET_INDENT = INDENT + INDENT; + + public CodeSnippet build(CodeBlock code) { + MethodSpec.Builder method = MethodSpec.methodBuilder("test") + .addModifiers(Modifier.PUBLIC); + CodeBlock.Builder body = CodeBlock.builder(); + body.add(START_SNIPPET); + body.add(code); + body.add(END_SNIPPET); + method.addCode(body.build()); + String fileContent = write(createTestJavaFile(method.build())); + String snippet = isolateGeneratedContent(fileContent); + return new CodeSnippet(fileContent, snippet); + } + + private String isolateGeneratedContent(String javaFile) { + int start = javaFile.indexOf(START_SNIPPET); + String tmp = javaFile.substring(start + START_SNIPPET.length()); + int end = tmp.indexOf(END_SNIPPET); + tmp = tmp.substring(0, end); + // Remove indent + return tmp.lines().map(line -> { + if (!line.startsWith(SNIPPET_INDENT)) { + throw new IllegalStateException("Missing indent for " + line); + } + return line.substring(SNIPPET_INDENT.length()); + }).collect(Collectors.joining("\n")); + } + + private JavaFile createTestJavaFile(MethodSpec method) { + return JavaFile.builder("example", TypeSpec.classBuilder("Test") + .addModifiers(Modifier.PUBLIC) + .addMethod(method).build()).indent(INDENT).build(); + } + + private String write(JavaFile file) { + try { + StringWriter out = new StringWriter(); + file.writeTo(out); + return out.toString(); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to write " + file, ex); + } + } + + } +} diff --git a/spring-core/src/main/java/org/springframework/javapoet/support/MultiCodeBlock.java b/spring-core/src/main/java/org/springframework/javapoet/support/MultiCodeBlock.java new file mode 100644 index 00000000000..f1391b62d5a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/javapoet/support/MultiCodeBlock.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.javapoet.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; + + +/** + * A {@link CodeBlock} wrapper for joining multiple blocks. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class MultiCodeBlock { + + private final List codeBlocks = new ArrayList<>(); + + + /** + * Add the specified {@link CodeBlock}. + * @param code the code block to add + */ + public void add(CodeBlock code) { + if (code.isEmpty()) { + throw new IllegalArgumentException("Could not add empty CodeBlock"); + } + this.codeBlocks.add(code); + } + + /** + * Add a {@link CodeBlock} using the specified callback. + * @param code the callback to use + */ + public void add(Consumer code) { + Builder builder = CodeBlock.builder(); + code.accept(builder); + add(builder.build()); + } + + /** + * Add a code block using the specified formatted String and the specified + * arguments. + * @param code the code + * @param arguments the arguments + * @see Builder#add(String, Object...) + */ + public void add(String code, Object... arguments) { + add(CodeBlock.of(code, arguments)); + } + + /** + * Return a {@link CodeBlock} that joins the different blocks registered in + * this instance with the specified delimiter. + * @param delimiter the delimiter to use (not {@literal null}) + * @return a {@link CodeBlock} joining the blocks of this instance with the + * specified {@code delimiter} + * @see CodeBlock#join(Iterable, String) + */ + public CodeBlock join(String delimiter) { + return CodeBlock.join(this.codeBlocks, delimiter); + } + +} diff --git a/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java b/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java new file mode 100644 index 00000000000..04bd02071e2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/javapoet/support/MultiStatement.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.javapoet.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; + + +/** + * A {@link CodeBlock} wrapper for multiple statements. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class MultiStatement { + + private final List statements = new ArrayList<>(); + + + /** + * Specify if this instance is empty. + * @return {@code true} if no statement is registered, {@code false} otherwise + */ + public boolean isEmpty() { + return this.statements.isEmpty(); + } + + /** + * Add the specified {@link CodeBlock codeblock} rendered as-is. + * @param codeBlock the code block to add + * @see #addStatement(CodeBlock) to add a code block that represents + * a statement + */ + public void add(CodeBlock codeBlock) { + this.statements.add(Statement.of(codeBlock)); + } + + /** + * Add a {@link CodeBlock} rendered as-is using the specified callback. + * @param code the callback to use + * @see #addStatement(CodeBlock) to add a code block that represents + * a statement + */ + public void add(Consumer code) { + CodeBlock.Builder builder = CodeBlock.builder(); + code.accept(builder); + add(builder.build()); + } + + /** + * Add a statement. + * @param statement the statement to add + */ + public void addStatement(CodeBlock statement) { + this.statements.add(Statement.ofStatement(statement)); + } + + /** + * Add a statement using the specified callback. + * @param code the callback to use + */ + public void addStatement(Consumer code) { + CodeBlock.Builder builder = CodeBlock.builder(); + code.accept(builder); + addStatement(builder.build()); + } + + /** + * Add a statement using the specified formatted String and the specified + * arguments. + * @param code the code of the statement + * @param args the arguments for placeholders + * @see CodeBlock#of(String, Object...) + */ + public void addStatement(String code, Object... args) { + addStatement(CodeBlock.of(code, args)); + } + + /** + * Add the statements produced from the {@code itemGenerator} applied on the specified + * items. + * @param items the items to handle, each item is represented as a statement + * @param itemGenerator the item generator + * @param the type of the item + */ + public void addAll(Iterable items, Function itemGenerator) { + items.forEach(element -> addStatement(itemGenerator.apply(element))); + } + + /** + * Return a {@link CodeBlock} that applies all the {@code statements} of this + * instance. If only one statement is available, it is not completed using the + * {@code ;} termination so that it can be used in the context of a lambda. + * @return the statement(s) + */ + public CodeBlock toCodeBlock() { + Builder code = CodeBlock.builder(); + for (int i = 0; i < this.statements.size(); i++) { + Statement statement = this.statements.get(i); + statement.contribute(code, this.isMulti(), i == this.statements.size() - 1); + } + return code.build(); + } + + /** + * Return a {@link CodeBlock} that applies all the {@code statements} of this + * instance in the context of a lambda. + * @param lambda the context of the lambda, must end with {@code ->} + * @return the lambda body + */ + public CodeBlock toCodeBlock(CodeBlock lambda) { + Builder code = CodeBlock.builder(); + code.add(lambda); + if (isMulti()) { + code.beginControlFlow(""); + } + else { + code.add(" "); + } + code.add(toCodeBlock()); + if (isMulti()) { + code.add("\n").unindent().add("}"); + } + return code.build(); + } + + /** + * Return a {@link CodeBlock} that applies all the {@code statements} of this + * instance in the context of a lambda. + * @param lambda the context of the lambda, must end with {@code ->} + * @return the lambda body + */ + public CodeBlock toCodeBlock(String lambda) { + return toCodeBlock(CodeBlock.of(lambda)); + } + + private boolean isMulti() { + return this.statements.size() > 1; + } + + + private static class Statement { + + private final CodeBlock codeBlock; + + private final boolean addStatementTermination; + + Statement(CodeBlock codeBlock, boolean addStatementTermination) { + this.codeBlock = codeBlock; + this.addStatementTermination = addStatementTermination; + } + + void contribute(CodeBlock.Builder code, boolean multi, boolean isLastStatement) { + code.add(this.codeBlock); + if (this.addStatementTermination) { + if (!isLastStatement) { + code.add(";\n"); + } + else if (multi) { + code.add(";"); + } + } + } + + static Statement ofStatement(CodeBlock codeBlock) { + return new Statement(codeBlock, true); + } + + static Statement of(CodeBlock codeBlock) { + return new Statement(codeBlock, false); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/javapoet/support/package-info.java b/spring-core/src/main/java/org/springframework/javapoet/support/package-info.java new file mode 100644 index 00000000000..6f2c37d88bc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/javapoet/support/package-info.java @@ -0,0 +1,9 @@ +/** + * Support classes for JavaPoet usage. + */ +@NonNullApi +@NonNullFields +package org.springframework.javapoet.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/test/java/org/springframework/javapoet/support/CodeSnippetTests.java b/spring-core/src/test/java/org/springframework/javapoet/support/CodeSnippetTests.java new file mode 100644 index 00000000000..d11383f2db3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/javapoet/support/CodeSnippetTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.javapoet.support; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.javapoet.CodeBlock; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CodeSnippet}. + * + * @author Stephane Nicoll + */ +class CodeSnippetTests { + + @Test + void snippetUsesTabs() { + CodeBlock.Builder code = CodeBlock.builder(); + code.beginControlFlow("if (condition)"); + code.addStatement("bean.doThis()"); + code.endControlFlow(); + CodeSnippet codeSnippet = CodeSnippet.of(code.build()); + assertThat(codeSnippet.getSnippet()).isEqualTo(""" + if (condition) { + bean.doThis(); + } + """); + } + + @Test + void snippetResolvesImports() { + CodeSnippet codeSnippet = CodeSnippet.of( + CodeBlock.of("$T list = new $T<>()", List.class, ArrayList.class)); + assertThat(codeSnippet.getSnippet()).isEqualTo("List list = new ArrayList<>()"); + assertThat(codeSnippet.hasImport(List.class)).isTrue(); + assertThat(codeSnippet.hasImport(ArrayList.class)).isTrue(); + } + + @Test + void removeIndent() { + CodeBlock.Builder code = CodeBlock.builder(); + code.beginControlFlow("if (condition)"); + code.addStatement("doStuff()"); + code.endControlFlow(); + CodeSnippet snippet = CodeSnippet.of(code.build()); + assertThat(snippet.getSnippet().lines()).contains("\tdoStuff();"); + assertThat(snippet.removeIndent(1).getSnippet().lines()).contains("doStuff();"); + } + + @Test + void processProvidesSnippet() { + assertThat(CodeSnippet.process(code -> code.add("$T list;", List.class))) + .isEqualTo("List list;"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/javapoet/support/MultiCodeBlockTests.java b/spring-core/src/test/java/org/springframework/javapoet/support/MultiCodeBlockTests.java new file mode 100644 index 00000000000..603aa10d3e5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/javapoet/support/MultiCodeBlockTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.javapoet.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.javapoet.CodeBlock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MultiCodeBlock}. + * + * @author Stephane Nicoll + */ +class MultiCodeBlockTests { + + @Test + void joinWithNoElement() { + MultiCodeBlock multi = new MultiCodeBlock(); + assertThat(multi.join(", ").isEmpty()).isTrue(); + } + + @Test + void joinWithEmptyElement() { + MultiCodeBlock multi = new MultiCodeBlock(); + assertThatIllegalArgumentException().isThrownBy(() -> multi.add(CodeBlock.builder().build())); + } + + @Test + void joinWithSingleElement() { + MultiCodeBlock multi = new MultiCodeBlock(); + multi.add(CodeBlock.of("$S", "Hello")); + assertThat(multi.join(", ")).hasToString("\"Hello\""); + } + + @Test + void joinWithSeveralElement() { + MultiCodeBlock multi = new MultiCodeBlock(); + multi.add(CodeBlock.of("$S", "Hello")); + multi.add(code -> code.add("42")); + multi.add("null"); + assertThat(multi.join(", ")).hasToString("\"Hello\", 42, null"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/javapoet/support/MultiStatementTests.java b/spring-core/src/test/java/org/springframework/javapoet/support/MultiStatementTests.java new file mode 100644 index 00000000000..da766c523cc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/javapoet/support/MultiStatementTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.javapoet.support; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.javapoet.CodeBlock; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MultiStatement}. + * + * @author Stephane Nicoll + */ +class MultiStatementTests { + + @Test + void isEmptyWithNoStatement() { + assertThat(new MultiStatement().isEmpty()).isTrue(); + } + + @Test + void isEmptyWithStatement() { + MultiStatement statements = new MultiStatement(); + statements.addStatement(CodeBlock.of("int i = 0")); + assertThat(statements.isEmpty()).isFalse(); + } + + @Test + void singleStatement() { + MultiStatement statements = new MultiStatement(); + statements.addStatement("field.method($S)", "hello"); + CodeBlock codeBlock = statements.toCodeBlock(); + assertThat(codeBlock.toString()).isEqualTo("field.method(\"hello\")"); + } + + @Test + void singleStatementWithCallback() { + MultiStatement statements = new MultiStatement(); + statements.addStatement(code -> code.add("field.method($S)", "hello")); + CodeBlock codeBlock = statements.toCodeBlock(); + assertThat(codeBlock.toString()).isEqualTo("field.method(\"hello\")"); + } + + @Test + void singleStatementWithCodeBlock() { + MultiStatement statements = new MultiStatement(); + statements.addStatement(CodeBlock.of("field.method($S)", "hello")); + CodeBlock codeBlock = statements.toCodeBlock(); + assertThat(codeBlock.toString()).isEqualTo("field.method(\"hello\")"); + } + + @Test + void multiStatements() { + MultiStatement statements = new MultiStatement(); + statements.addStatement("field.method($S)", "hello"); + statements.addStatement("field.anotherMethod($S)", "hello"); + CodeBlock codeBlock = statements.toCodeBlock(); + assertThat(codeBlock.toString()).isEqualTo(""" + field.method("hello"); + field.anotherMethod("hello");"""); + } + + @Test + void multiStatementsWithCodeBlockRenderedAsIs() { + MultiStatement statements = new MultiStatement(); + statements.addStatement("field.method($S)", "hello"); + statements.add(CodeBlock.of(("// Hello\n"))); + statements.add(code -> code.add("// World\n")); + statements.addStatement("field.anotherMethod($S)", "hello"); + CodeBlock codeBlock = statements.toCodeBlock(); + assertThat(codeBlock.toString()).isEqualTo(""" + field.method("hello"); + // Hello + // World + field.anotherMethod("hello");"""); + } + + @Test + void singleStatementWithLambda() { + MultiStatement statements = new MultiStatement(); + statements.addStatement("field.method($S)", "hello"); + CodeBlock codeBlock = statements.toCodeBlock(CodeBlock.of("() ->")); + assertThat(codeBlock.toString()).isEqualTo("() -> field.method(\"hello\")"); + } + + @Test + void multiStatementsWithLambda() { + MultiStatement statements = new MultiStatement(); + statements.addStatement("field.method($S)", "hello"); + statements.addStatement("field.anotherMethod($S)", "hello"); + CodeBlock codeBlock = statements.toCodeBlock(CodeBlock.of("() ->")); + assertThat(codeBlock.toString().lines()).containsExactly( + "() -> {", + " field.method(\"hello\");", + " field.anotherMethod(\"hello\");", + "}"); + } + + @Test + void multiStatementsWithAddAll() { + MultiStatement statements = new MultiStatement(); + statements.addAll(List.of(0, 1, 2), + index -> CodeBlock.of("field[$L] = $S", index, "hello")); + CodeBlock codeBlock = statements.toCodeBlock("() ->"); + assertThat(codeBlock.toString().lines()).containsExactly( + "() -> {", + " field[0] = \"hello\";", + " field[1] = \"hello\";", + " field[2] = \"hello\";", + "}"); + } + +}