diff --git a/src/main/java/com/squareup/javapoet/CodeBlock.java b/src/main/java/com/squareup/javapoet/CodeBlock.java index 5376984..ebedbd6 100644 --- a/src/main/java/com/squareup/javapoet/CodeBlock.java +++ b/src/main/java/com/squareup/javapoet/CodeBlock.java @@ -21,11 +21,14 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import static com.squareup.javapoet.Util.checkArgument; @@ -415,6 +418,156 @@ public Builder add(CodeBlock codeBlock) { return this; } + /** + * Structures a lambda function based on some inputs and a CodeBlock body.
+ * Should be used with {@link #add(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((int x, int y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param body the body of the function. + */ + public Builder addLambda(Iterable parameters, LambdaMode mode, CodeBlock body) { + // check for specification of input type visibility + boolean emitTypes = mode.equals(LambdaMode.VISIBLE_TYPES); + + Stream parameterStream = StreamSupport.stream( + parameters.spliterator(), + false + ); + + // the inputs of the lambda (left side) + String inputSide = parameterStream + .peek(p -> checkArgument(!p.type.equals(TypeName.VOID), // validate the input types + "lambda input parameters cannot be of void type") + ) + .map(p -> emitTypes ? p.toString() : p.name) + .collect(Collectors.joining(", ")); + + // the count of given inputs + int paramsLen = inputSide.split(",").length; + + // on 0 or more than 1 inputs, or in case of type emission, + // parentheses are mandatory + if (paramsLen > 1 || emitTypes || inputSide.isEmpty()) { + inputSide = "(" + inputSide + ")"; + } + + // the body of the lambda (right side) + String bodySide = body.toString().replaceAll("\n", ""); + + // in case of multiple statements, braces are mandatory + if (bodySide.contains(";")) { + bodySide = "{" + bodySide + "}"; + } + + // the full lambda structure + add(inputSide + " -> " + bodySide); + return this; + } + + /** + * Structures a lambda function based on some inputs and a CodeBlock body.
+ * Will not emit the input types.
+ * Should be used with {@link #add(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((x, y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param body the body of the function. + */ + public Builder addLambda(Iterable parameters, CodeBlock body) { + return addLambda(parameters, LambdaMode.DEFAULT, body); + } + + /** + * Structures a lambda function based on some inputs and an expression body.
+ * Should be used with {@link #add(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((int x, int y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(Iterable parameters, LambdaMode mode, + String expressionFormat, Object... args) { + return addLambda(parameters, mode, CodeBlock.of(expressionFormat, args)); + } + + /** + * Structures a lambda function based on some inputs and an expression body.
+ * Will not emit the input types.
+ * Should be used with {@link #add(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((x, y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(Iterable parameters, String expressionFormat, Object... args) { + return addLambda(parameters, LambdaMode.DEFAULT, CodeBlock.of(expressionFormat, args)); + } + + /** + * Structures a producer lambda function based on a CodeBlock body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param body the body of the lambda. + */ + public Builder addLambda(LambdaMode mode, CodeBlock body) { + return addLambda(Collections.emptyList(), mode, body); + } + + /** + * Structures a producer lambda function based on a CodeBlock body.
+ * Should be used with {@link #add(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param body the body of the lambda. + */ + public Builder addLambda(CodeBlock body) { + return addLambda(Collections.emptyList(), LambdaMode.DEFAULT, body); + } + + /** + * Structures a producer lambda function based on an expression body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(LambdaMode mode, String expressionFormat, Object... args) { + return addLambda(Collections.emptyList(), mode, expressionFormat, args); + } + + /** + * Structures a producer lambda function based on an expression body.
+ * Should be used with {@link #add(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(String expressionFormat, Object... args) { + return addLambda(Collections.emptyList(), LambdaMode.DEFAULT, expressionFormat, args); + } + public Builder indent() { this.formatParts.add("$>"); return this; diff --git a/src/main/java/com/squareup/javapoet/LambdaMode.java b/src/main/java/com/squareup/javapoet/LambdaMode.java new file mode 100644 index 0000000..503101b --- /dev/null +++ b/src/main/java/com/squareup/javapoet/LambdaMode.java @@ -0,0 +1,11 @@ +package com.squareup.javapoet; + +/** + * The available format modes a lambda function can have. + * {@code DEFAULT} example: (x, y) -> x + y; + * {@code VISIBLE_TYPES} example: (int x, int y) -> x + y; +*/ +public enum LambdaMode { + DEFAULT, + VISIBLE_TYPES +} diff --git a/src/main/java/com/squareup/javapoet/MethodSpec.java b/src/main/java/com/squareup/javapoet/MethodSpec.java index 6914858..e258326 100644 --- a/src/main/java/com/squareup/javapoet/MethodSpec.java +++ b/src/main/java/com/squareup/javapoet/MethodSpec.java @@ -296,21 +296,25 @@ public static final class Builder { private String name; private final CodeBlock.Builder javadoc = CodeBlock.builder(); + public final List annotations = new ArrayList<>(); + public final List modifiers = new ArrayList<>(); + public final List typeVariables = new ArrayList<>(); private TypeName returnType; + public final List parameters = new ArrayList<>(); + private boolean varargs; private final Set exceptions = new LinkedHashSet<>(); private final CodeBlock.Builder code = CodeBlock.builder(); - private boolean varargs; private CodeBlock defaultValue; - public final List typeVariables = new ArrayList<>(); - public final List annotations = new ArrayList<>(); - public final List modifiers = new ArrayList<>(); - public final List parameters = new ArrayList<>(); private Builder(String name) { setName(name); } + /** + * Sets a name for this builder. + * @param name the name to be set for the method. + */ public Builder setName(String name) { checkNotNull(name, "name == null"); checkArgument(name.equals(CONSTRUCTOR) || SourceVersion.isName(name), @@ -500,6 +504,10 @@ public Builder nextControlFlow(CodeBlock codeBlock) { return nextControlFlow("$L", codeBlock); } + /** + * Ends the last open control flow. + * Should be used once for every control flow. + */ public Builder endControlFlow() { code.endControlFlow(); return this; @@ -532,6 +540,130 @@ public Builder addStatement(CodeBlock codeBlock) { return this; } + /** + * Structures a lambda function based on some inputs and a CodeBlock body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((int x, int y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * be emitted on the result. + * @param body the body of the function. + */ + public Builder addLambda(Iterable parameters, LambdaMode mode, CodeBlock body) { + code.addLambda(parameters, mode, body); + return this; + } + + /** + * Structures a lambda function based on some inputs and a CodeBlock body.
+ * Will not emit the input types.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((x, y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param body the body of the function. + */ + public Builder addLambda(Iterable parameters, CodeBlock body) { + code.addLambda(parameters, LambdaMode.DEFAULT, body); + return this; + } + + /** + * Structures a lambda function based on some inputs and an expression body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((int x, int y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(Iterable parameters, LambdaMode mode, + String expressionFormat, Object... args) { + code.addLambda(parameters, mode, CodeBlock.of(expressionFormat, args)); + return this; + } + + /** + * Structures a lambda function based on some inputs and an expression body.
+ * Will not emit the input types.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall((x, y) -> x + y, 5). + * @param parameters the input parameters of the function. + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(Iterable parameters, String expressionFormat, Object... args) { + code.addLambda(parameters, LambdaMode.DEFAULT, CodeBlock.of(expressionFormat, args)); + return this; + } + + /** + * Structures a producer lambda function based on a CodeBlock body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param body the body of the lambda. + */ + public Builder addLambda(LambdaMode mode, CodeBlock body) { + code.addLambda(Collections.emptyList(), mode, body); + return this; + } + + /** + * Structures a producer lambda function based on a CodeBlock body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param body the body of the lambda. + */ + public Builder addLambda(CodeBlock body) { + code.addLambda(Collections.emptyList(), LambdaMode.DEFAULT, body); + return this; + } + + /** + * Structures a producer lambda function based on an expression body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param mode the format mode that should be used. + * @see com.squareup.javapoet.LambdaMode LambdaMode. + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(LambdaMode mode, String expressionFormat, Object... args) { + code.addLambda(Collections.emptyList(), mode, expressionFormat, args); + return this; + } + + /** + * Structures a producer lambda function based on an expression body.
+ * Should be used with {@link #addCode(String, Object...) addCode}, + * to provide specific behaviour such as + * methodcall(() -> 3 + 2, 5). + * @param expressionFormat the format that should be used + * for the expression. + * @param args the values that should be placed in the holders + * of the format. + */ + public Builder addLambda(String expressionFormat, Object... args) { + code.addLambda(Collections.emptyList(), LambdaMode.DEFAULT, expressionFormat, args); + return this; + } + public MethodSpec build() { return new MethodSpec(this); } diff --git a/src/test/java/com/squareup/javapoet/MethodSpecTest.java b/src/test/java/com/squareup/javapoet/MethodSpecTest.java index 56cc3b7..50eb992 100644 --- a/src/test/java/com/squareup/javapoet/MethodSpecTest.java +++ b/src/test/java/com/squareup/javapoet/MethodSpecTest.java @@ -482,8 +482,123 @@ abstract static class AbstractClassWithPrivateAnnotation { "}\n"); } - private static CodeBlock named(String format, Map args){ + private static CodeBlock named(String format, Map args) { return CodeBlock.builder().addNamed(format, args).build(); } + @Test public void ensureLambdaTypeError() { + // parameter with void type - should be rejected + ParameterSpec p1 = ParameterSpec.builder(TypeName.VOID, "x").build(); + + try { + // check that void type will cause errors + CodeBlock.builder() + .addLambda(List.of(p1), "$N * 2", "x"); + + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo( + "lambda input parameters cannot be of void type" + ); + } + } + + @Test public void ensureLambdaMethodSpecLiability() { + // lambda inputs + ParameterSpec p1 = ParameterSpec.builder(TypeName.INT, "x").build(); + ParameterSpec p2 = ParameterSpec.builder(TypeName.DOUBLE, "y").build(); + + // lambda body that considers input values + CodeBlock body1 = CodeBlock.of("int $3N = 3; return $1N + $2N + $3N;", "x", "y", "z"); + + // lambda body that does not consider input values + CodeBlock body2 = CodeBlock.of("int $1N = 3; int $2N = 5; return $1N + $2N;", "x", "y"); + + MethodSpec method = MethodSpec.methodBuilder("method") + .addCode("methodCall(") + .addLambda(List.of(p1, p2), LambdaMode.VISIBLE_TYPES, body1) // lambda with multiple inputs and (CodeBlock) body + .addCode(", ") + .addLambda(List.of(p2), + "$N + $N", "x", "y") // lambda with single input and (String) body + .addCode(", ") + .addLambda(body2) // lambda with no inputs and (CodeBlock) body + .addCode(", ") + .addLambda("5 + 7") // lambda with no inputs and (String) body + .addCode(", ") + .addLambda("method1(); method2();") // lambda with multiple statements + .addCode(", ") + .addLambda(List.of(p1), LambdaMode.VISIBLE_TYPES, "x + 5") // lambda with single input of emitted type + .addCode(");\n") + .build(); + + assertThat(method.toString()).isEqualTo( + "void method() {\n" + + " methodCall(" + + "(int x, double y) -> {int z = 3; return x + y + z;}, " + + "y -> x + y, " + + "() -> {int x = 3; int y = 5; return x + y;}, " + + "() -> 5 + 7, " + + "() -> {method1(); method2();}, " + + "(int x) -> x + 5" + + ");\n" + + "}\n" + ); + } + + @Test public void ensureLambdaCodeBlockLiability() { + // lambda body that does not consider input values + CodeBlock body = CodeBlock.of("int $1N = 3; int $2N = 5; return $1N + $2N;", "x", "y"); + + // lambda expression that does not consider input values + CodeBlock body2 = CodeBlock.of("5 + 3"); + + CodeBlock codeWithLambda = CodeBlock.builder() + .add("methodCall(") + .addLambda(body) // producer lambda + .add(", ") + .addLambda(body2) + .add(");") + .build(); + + MethodSpec method = MethodSpec.methodBuilder("method") + .addCode(codeWithLambda) + .build(); + + assertThat(method.toString()).isEqualTo( + "void method() {\n" + + " methodCall(" + + "() -> {int x = 3; int y = 5; return x + y;}, " + + "() -> 5 + 3" + + ");\n" + + "}\n" + ); + } + + @Test public void ensureLambdaModeLiability() { + // lambda body that does not consider input values + CodeBlock body = CodeBlock.of("int $1N = 3; int $2N = 5; return $1N + $2N;", "x", "y"); + + CodeBlock codeWithLambda = CodeBlock.builder() + .add("methodCall(") + .addLambda(LambdaMode.VISIBLE_TYPES, "5 + 3") // ensure that redundant mode specification doesnt break anything + .add(");\n") + .build(); + + MethodSpec method = MethodSpec.methodBuilder("method") + .addCode(codeWithLambda) + .addCode("Producer x = ") + .addLambda(LambdaMode.VISIBLE_TYPES, body) // ensure that redundant mode specification doesnt break anything + .addCode(";") + .build(); + + assertThat(method.toString()).isEqualTo( + "void method() {\n" + + " methodCall(" + + "() -> 5 + 3" + + ");\n" + + " Producer x = () -> {int x = 3; int y = 5; return x + y;};\n" + + "}\n" + ); + } + }