Skip to content
This repository has been archived by the owner on Oct 10, 2024. It is now read-only.

feat: $V for inlined values in generated code #999

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,144 @@ public final class HelloWorld {
}
```

### $V for inlined values

Sometimes you have an object that you want to reconstruct in generated code.
But that object has over 20 fields, and each of those fields has over 20 fields.
To simplify this reconstruction process, you can use **`$V`** to emit an **inlined** value:

```java
import com.squareup.javapoet.ObjectInliner;

private MethodSpec computeSomething(String name, MyComplexConfig config) {
ObjectInliner inliner = new ObjectInliner()
.trustExactTypes(MyComplexConfig.class);
return MethodSpec.methodBuilder(name)
.returns(int.class)
.addStatement("return MyComplexCalculation.calculate($V);", inliner.inlined(config))
.build();
}
```

Inlined values are constructed inside a generated `java.util.function.Supplier` lambda that constructs
the value from its fields.
For the above example, the generated code might look like this:
```java
int name() {
return MyComplexCalculation.calculate(
((MyComplexConfig)((java.util.function.Supplier)(() -> {
MyComplexConfig $$javapoet$MyComplexConfig = new MyComplexConfig();
$$javapoet$MyComplexConfig.setParameter1(1);
$$javapoet$MyComplexConfig.setParameter2("abc");
// ...
return $$javapoet$MyComplexConfig;
})).get()));
}
```
The generated code will invoke getters and setters on the passed instance,
so make sure you **trust** any values you inline with **`$V`**.
By default, only `java.lang.Object` is trusted.
You can trust additional object types by calling `trustExactTypes` and
`trustTypesAssignableTo`:

```java
import com.squareup.javapoet.ObjectInliner;

private MethodSpec computeSomething(String name, MyComplexConfig config) {
ObjectInliner inliner = new ObjectInliner()
.trustTypesAssignableTo(List.class)
.trustExactTypes(MyComplexConfig.class);
//...
}
```
If you trust everything, you can call `trustEverything`. Although please don't do this with user controlled instances.

```java
import com.squareup.javapoet.ObjectInliner;

private MethodSpec computeSomething(String name, MyComplexConfig config) {
ObjectInliner inliner = new ObjectInliner()
.trustEverything();
//...
}
```

If you don't pass an instance of `ObjectInliner.Inlined`, it will be
inlined by `ObjectInliner.getDefault()`.

```java
import com.squareup.javapoet.ObjectInliner;

private MethodSpec computeSomething(String name, MyComplexConfig config) {
ObjectInliner.getDefault()
.trustEverything();
return MethodSpec.methodBuilder(name)
.returns(int.class)
// config will be inlined by ObjectInliner.getDefault()
.addStatement("return MyComplexCalculation.calculate($V);", config)
.build();
}
```

By default, the following objects can be inlined if trusted:

- Primitives and their boxed types (trusted by default)
- String (trusted by default)
- `java.lang.Class` (trusted by default)
- Instances of `java.lang.Enum` (trusted by default)
- Arrays of inlinable trusted objects (trusted by default)
- Lists, Sets, and Maps of inlineable trusted objects (not trusted by default)
- Objects that have public setters for all non-public fields (not trusted by default)
- Records of inlineable trusted objects (not trusted by default)

You can register custom inliners for types not covered by the above using `TypeInliner`:

```java
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.ObjectEmitter;
import com.squareup.javapoet.TypeInliner;
import com.squareup.javapoet.ObjectInliner;

import java.time.Duration;

private MethodSpec computeSomething(String name, MyComplexConfig config) {
ObjectInliner.getDefault()
.trustEverything()
.addTypeInliner(new TypeInliner() {
@Override
public boolean canInline(Object object) {
return object instanceof Duration;
}

@Override
public String inline(ObjectEmitter emitter, Object instance) {
Duration duration = (Duration) instance;
return CodeBlock.of("$T.ofNanos($V);", Duration.class, emitter.inlined(duration.toNanos())).toString();
}
});
return MethodSpec.methodBuilder(name)
.returns(int.class)
.addStatement("return MyComplexCalculation.calculate($V);", Duration.ofSeconds(1L))
.build();
}
```

If the default name prefix `$$javapoet$` conflicts with any variables in your generated code,
you can specify a different one using `useNamePrefix`:

```java
import com.squareup.javapoet.ObjectInliner;

private MethodSpec computeSomething(String name, MyComplexConfig config) {
ObjectInliner.getDefault()
.useNamePrefix("myPrefix");
return MethodSpec.methodBuilder(name)
.returns(int.class)
.addStatement("return MyComplexCalculation.calculate($V);", config)
.build();
}
```

#### Import static

JavaPoet supports `import static`. It does it via explicitly collecting type member names. Let's
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/squareup/javapoet/CodeBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@
* <li>{@code $T} emits a <em>type</em> reference. Types will be imported if possible. Arguments
* for types may be {@linkplain Class classes}, {@linkplain javax.lang.model.type.TypeMirror
,* type mirrors}, and {@linkplain javax.lang.model.element.Element elements}.
* <li>{@code $V} emits an <em>inlined-value</em>. The inlined-value can be a primitive,
* {@linkplain Enum an enum value}, {@link Class a class constant}, an instance of a class
* with setters for all non-public instance fields, and arrays, collections, and maps
* containing inlinable values. An inlined-value can be assigned to a variable, passed
* as a parameter to a method, and generally can be used as an object with all its methods
* available for use. The inlined-value will be a raw type; cast to a generic type if
* needed.
* <li>{@code $$} emits a dollar sign.
* <li>{@code $W} emits a space or a newline, depending on its position on the line. This prefers
* to wrap lines before 100 columns.
Expand Down Expand Up @@ -329,6 +336,9 @@ private void addArgument(String format, char c, Object arg) {
case 'T':
this.args.add(argToType(arg));
break;
case 'V':
this.args.add(argToInlinedValue(arg));
break;
default:
throw new IllegalArgumentException(
String.format("invalid format string: '%s'", format));
Expand Down Expand Up @@ -360,6 +370,13 @@ private TypeName argToType(Object o) {
throw new IllegalArgumentException("expected type but was " + o);
}

private ObjectInliner.Inlined argToInlinedValue(Object o) {
if (o instanceof ObjectInliner.Inlined) {
return (ObjectInliner.Inlined) o;
}
return ObjectInliner.getDefault().inlined(o);
}

/**
* @param controlFlow the control flow construct and its code, such as "if (foo == 5)".
* Shouldn't contain braces or newline characters.
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/squareup/javapoet/CodeWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ public CodeWriter emit(CodeBlock codeBlock, boolean ensureTrailingNewline) throw
typeName.emit(this);
break;

case "$V":
ObjectInliner.Inlined inlined = (ObjectInliner.Inlined) codeBlock.args.get(a++);
inlined.emit(this);
break;

case "$$":
emitAndIndent("$");
break;
Expand Down
Loading