This repository was archived by the owner on Aug 11, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Add Lambda support through Lambdaspec class #16
Open
HliasMpGH
wants to merge
2
commits into
Randgalt:master
Choose a base branch
from
HliasMpGH:lambdaspec-addition
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| package com.squareup.javapoet; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| import javax.lang.model.element.Modifier; | ||
| import static com.squareup.javapoet.Util.checkArgument; | ||
| import static com.squareup.javapoet.Util.checkNotNull;; | ||
|
|
||
| public class LambdaSpec { | ||
|
|
||
| /** | ||
| * The characters that will be placed *before* | ||
| * the *arrow* of the lambda. | ||
| */ | ||
| private final String beforeArrow; | ||
|
|
||
| /** The inputs of the function. */ | ||
| private final List<ParameterSpec> inputs; | ||
|
|
||
| /** | ||
| * The characters that will be placed *before* | ||
| * the *body* of the lambda (and before the | ||
| * curly bracket in such cases). | ||
| */ | ||
| private final String beforeBody; | ||
|
|
||
| /** The body of the function. */ | ||
| private final CodeBlock body; | ||
|
|
||
| /** | ||
| * The characters that will be placed *after* | ||
| * the *body* of the lambda (and after the | ||
| * curly bracket in such cases). | ||
| */ | ||
| private final String afterBody; | ||
|
|
||
| /** The visibility status of the input parameters */ | ||
| private boolean visibleTypes; | ||
|
|
||
| private LambdaSpec(Builder builder) { | ||
| this.inputs = checkNotNull(builder.inputs, "inputs == null"); | ||
| validateInputs(this.inputs); | ||
| this.body = checkNotNull(builder.body, "body == null"); | ||
| this.beforeArrow = checkNotNull(builder.beforeArrow, "beforeArrow == null"); | ||
| this.beforeBody = checkNotNull(builder.beforeBody, "beforeBody == null"); | ||
| this.afterBody = checkNotNull(builder.afterBody, "afterBody == null"); | ||
| this.visibleTypes = builder.visibleTypes; | ||
| } | ||
|
|
||
| /** | ||
| * Emits the left hand side of the lambda function, meaning | ||
| * the inputs. | ||
| * @param codeWriter the writer used to append the constructed | ||
| * inputs. | ||
| * @throws IOException | ||
| */ | ||
| void emitInputs(CodeWriter codeWriter) throws IOException { | ||
| boolean placeParenthesis = inputs.size() > 1 || visibleTypes || inputs.isEmpty(); | ||
|
|
||
| codeWriter.emit(placeParenthesis ? "(" : ""); | ||
|
|
||
| int paramsPlaced = 0; | ||
|
|
||
| // the inputs of the lambda (left side) | ||
| for (ParameterSpec inputParameter : inputs) { | ||
| if (visibleTypes) { | ||
| codeWriter.emitAnnotations(inputParameter.annotations, true); | ||
| codeWriter.emitModifiers(inputParameter.modifiers); | ||
| codeWriter.emit(inputParameter.type.toString() + " "); | ||
| } | ||
| codeWriter.emit(inputParameter.name); | ||
| if (++paramsPlaced < inputs.size()) | ||
| codeWriter.emit(", "); | ||
| } | ||
|
|
||
| codeWriter.emit(placeParenthesis ? ")" : ""); | ||
| } | ||
|
|
||
| /** | ||
| * Emits the right hand side of the lambda function, meaning | ||
| * the body. | ||
| * @param codeWriter the writer used to append the constructed | ||
| * body. | ||
| * @throws IOException | ||
| */ | ||
| void emitBody(CodeWriter codeWriter) throws IOException { | ||
| String bodySide = this.body.toString(); | ||
|
|
||
| // true if the function has more than 1 statement in its body | ||
| boolean multiStatementBody = bodySide.contains(";"); | ||
|
|
||
| codeWriter.emit(multiStatementBody ? "{" : ""); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if someone wants braces to start on newlines ala: We should support that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I can add more modes that support said format. Thanks for the suggestion. |
||
| codeWriter.emit(body); | ||
| codeWriter.emit(multiStatementBody ? "}" : ""); | ||
| } | ||
|
|
||
| @Override public boolean equals(Object o) { | ||
| if (this == o) return true; | ||
| if (o == null) return false; | ||
| if (getClass() != o.getClass()) return false; | ||
| return toString().equals(o.toString()); | ||
| } | ||
|
|
||
| @Override public int hashCode() { | ||
| return toString().hashCode(); | ||
| } | ||
|
|
||
| @Override public String toString() { | ||
| StringBuilder out = new StringBuilder(); | ||
| try { | ||
| CodeWriter codeWriter = new CodeWriter(out); | ||
| emitInputs(codeWriter); | ||
| codeWriter.emit(this.beforeArrow); | ||
| codeWriter.emit("->"); | ||
| codeWriter.emit(this.beforeBody); | ||
| emitBody(codeWriter); | ||
| codeWriter.emit(this.afterBody); | ||
| return out.toString(); | ||
| } catch (IOException e) { | ||
| throw new AssertionError(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates a list of parameters as lambda inputs. | ||
| * @param inputParameters the parameters of the lambda. | ||
| */ | ||
| private static void validateInputs(List<ParameterSpec> inputParameters) { | ||
| for (ParameterSpec inputParameter : inputParameters) { | ||
| checkArgument( | ||
| !inputParameter.type.equals(TypeName.VOID), | ||
| "lambda input parameters cannot be of void type" | ||
| ); | ||
| checkArgument( | ||
| inputParameter.modifiers.stream().allMatch(p -> p.equals(Modifier.FINAL)), | ||
| "lambda input parameters can only be final" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public static Builder builder(List<ParameterSpec> inputs, CodeBlock body) { | ||
| return new Builder(inputs, body); | ||
| } | ||
|
|
||
| public static Builder builder(List<ParameterSpec> inputs, String body) { | ||
| return builder(inputs, CodeBlock.of(body)); | ||
| } | ||
|
|
||
| public static Builder builder(CodeBlock body) { | ||
| return new Builder(body); | ||
| } | ||
|
|
||
| public static Builder builder(String body) { | ||
| return builder(CodeBlock.of(body)); | ||
| } | ||
|
|
||
| public Builder toBuilder() { | ||
| return toBuilder(inputs, body); | ||
| } | ||
|
|
||
| Builder toBuilder(List<ParameterSpec> inputs, CodeBlock body) { | ||
| Builder builder = new Builder(inputs, body); | ||
| return builder; | ||
| } | ||
|
|
||
| public static final class Builder { | ||
| public String beforeArrow = " "; | ||
| public List<ParameterSpec> inputs = new ArrayList<>(); | ||
| public String beforeBody = " "; | ||
| public CodeBlock body; | ||
| public String afterBody = ""; | ||
| public boolean visibleTypes; | ||
|
|
||
| private Builder(List<ParameterSpec> inputs, CodeBlock body) { | ||
| this.inputs = inputs; | ||
| this.body = body; | ||
| } | ||
|
|
||
| private Builder(List<ParameterSpec> inputs, String body) { | ||
| this(inputs, CodeBlock.of(body)); | ||
| } | ||
|
|
||
| private Builder(CodeBlock body) { | ||
| this.body = body; | ||
| } | ||
|
|
||
| private Builder(String body) { | ||
| this(CodeBlock.of(body)); | ||
| } | ||
|
|
||
| /** Adds an input to the function. */ | ||
| public Builder addInput(ParameterSpec... parameters) { | ||
| Collections.addAll(this.inputs, parameters); | ||
| return this; | ||
| } | ||
|
|
||
| public Builder beforeArrow(String code) { | ||
| this.beforeArrow = code; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder beforeBody(String code) { | ||
| this.beforeBody = code; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder afterBody(String code) { | ||
| this.afterBody = code; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder visibleTypes() { | ||
| this.visibleTypes = true; | ||
| return this; | ||
| } | ||
|
|
||
| public LambdaSpec build() { | ||
| return new LambdaSpec(this); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
src/test/java/com/squareup/javapoet/LambdaSpecTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| package com.squareup.javapoet; | ||
|
|
||
| import javax.lang.model.element.Modifier; | ||
|
|
||
| import static com.google.common.truth.Truth.assertThat; | ||
| import static org.junit.Assert.fail; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
|
|
||
| public class LambdaSpecTest { | ||
|
|
||
| /** Ensure that void inputs result in exception. */ | ||
| @Test public void ensureInputTypeError() { | ||
| // parameter with void type - should be rejected | ||
| ParameterSpec p1 = ParameterSpec.builder(TypeName.VOID, "x").build(); | ||
|
|
||
| try { | ||
| // check that void type will cause errors | ||
| LambdaSpec.builder("2 * x").addInput(p1).build(); | ||
|
|
||
| fail("Managed to create a lambda with void type input."); | ||
| } catch (IllegalArgumentException expected) { | ||
| assertThat(expected).hasMessageThat().isEqualTo( | ||
| "lambda input parameters cannot be of void type" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** Ensure that not all type inputs are allowed. */ | ||
| @Test public void ensureInputModifierError() { | ||
| // parameter with modifier different than final - should be rejected | ||
| ParameterSpec p1 = ParameterSpec.builder(TypeName.INT, "x") | ||
| .addModifiers(Modifier.PUBLIC).build(); | ||
|
|
||
| try { | ||
| // check that input will cause errors | ||
| LambdaSpec.builder("2 * x").addInput(p1).build(); | ||
|
|
||
| fail("Managed to create a lambda with input of public access."); | ||
| } catch (IllegalArgumentException expected) { | ||
| assertThat(expected).hasMessageThat().isEqualTo( | ||
| "lambda input parameters can only be final" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| @Test public void ensureMethodSpecCompatibility() { | ||
| // 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"); | ||
|
|
||
| // lambdas of different nature | ||
| LambdaSpec lambda1 = LambdaSpec.builder(body1).addInput(p1, p2).visibleTypes().build(); | ||
| LambdaSpec lambda2 = LambdaSpec.builder("x + y").addInput(p2).build(); | ||
| LambdaSpec lambda3 = LambdaSpec.builder(body2).build(); | ||
| LambdaSpec lambda4 = LambdaSpec.builder("5 + 7").build(); | ||
| LambdaSpec lambda5 = LambdaSpec.builder("method1(); method2();").build(); | ||
| LambdaSpec lambda6 = LambdaSpec.builder("x + 5").addInput(p1).visibleTypes().build(); | ||
|
|
||
| MethodSpec method = MethodSpec.methodBuilder("method") | ||
| .addCode("methodCall(") | ||
| .addLambda(lambda1).addCode(", ") // lambda with multiple inputs and (CodeBlock) body and visible types | ||
| .addLambda(lambda2).addCode(", ") // lambda with single input and (String) body | ||
| .addLambda(lambda3).addCode(", ") // lambda with no inputs and (CodeBlock) body | ||
| .addLambda(lambda4).addCode(", ") // lambda with no inputs and (String) body | ||
| .addLambda(lambda5).addCode(", ") // lambda with multiple statements | ||
| .addLambda(lambda6).addCode(");\n") // lambda with single input of visible type | ||
| .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 ensureCodeBlockCompatibility() { | ||
| // 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"); | ||
|
|
||
| LambdaSpec lambda1 = LambdaSpec.builder(body).build(); | ||
| LambdaSpec lambda2 = LambdaSpec.builder(body2).build(); | ||
| LambdaSpec lambda3 = LambdaSpec.builder(body).beforeBody("\n ").build(); | ||
|
|
||
| CodeBlock codeWithLambda = CodeBlock.builder() | ||
| .add("methodCall(") | ||
| .addLambda(lambda1).add(", ") // producer lambda | ||
| .addLambda(lambda2).add(");\n") | ||
| .add("Producer<Integer> pr = ") | ||
| .addLambda(lambda3) | ||
| .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" | ||
| + " Producer<Integer> pr = () ->\n" | ||
| + " {int x = 3; int y = 5; return x + y;}\n" | ||
| + "}\n" | ||
| ); | ||
| } | ||
|
|
||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will break if
;is in a string, etc. This seems like a fragile way of checking for multi statements. Can something be added toCodeWriterto signal more than 1 statement?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats an interesting thought. I will look into that
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "issue" with CodeWriter is that the components can always use the generic .emit() method, to append code. In that case, the CodeWriter is not aware of how many lines of code its about to append (it just knows that its about to append a String, its not like it will append it line-by-line). The same is true for CodeBlocks (with the .add() method). So essentialy, the problem we are facing is still, given a some code in a string, count how many valid ';' it has. We can probably get over it by the utilization of a parser library. What do you think?
EDIT: A contender for the parser can be the javaparser library.
Here is a quick example of how it can be performed: