Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

$R as MemberRef #496

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Expand Up @@ -360,6 +360,77 @@ class HelloWorld {
}
```

### $R for References

When generating code, you may point to already compiled elements like methods, fields
and especially the `enum` constant fields. Enter `$R`:

```java
CodeBlock.builder().add("$R", Thread.State.NEW).build();
```

That produces the fully-qualified member name:

```java
java.lang.Thread.State.NEW
```

A more complex example with ex- and implicit `MemberRef` creations for methods, fields, enum
constants:

```java
Method convert = TimeUnit.class.getMethod("convert", long.class, TimeUnit.class);

MethodSpec.Builder method = MethodSpec.methodBuilder("minutesToSeconds")
.addModifiers(Modifier.STATIC)
.returns(long.class)
.addParameter(long.class, "minutes")
.addStatement("return $R.$R(minutes, $R)", TimeUnit.SECONDS, convert, TimeUnit.MINUTES);

TypeSpec util = TypeSpec.classBuilder("Util")
.addMethod(method)
.build();

JavaFile.builder("readme", util).build();
```

That generates the following `.java` file, complete with the necessary `import`s:

```java
package readme;

import java.util.concurrent.TimeUnit;

class Util {
static long minutesToSeconds(long minutes) {
return TimeUnit.SECONDS.convert(minutes, TimeUnit.MINUTES);
}
}
```

`$R` lets you reference **static** members:
* enum constants, like e.g. `TimeUnit.MINUTES` above
* static fields, like `String.CASE_INSENSITIVE_ORDER`
* static methods, like `Class.forName(String)`

References to **instance** members are also supported. Here, it's up to you providing some object
as a call target. You can use any literal including `"this"`, `"super"`, `TypeName`s and
even `MemberRef`erences like can be seen with `TimeUnit.SECONDS` in the example above.

The `MemberRef` API allows optional type arguments as parameters. Those can be used to specify
the generic return type of a method:

```java
MemberRef emptyList = MemberRef.get(Collections.class.getMethod("emptyList"), String.class);
CodeBlock.builder().add("$R()", emptyList).build();
```

Which yields:

```java
java.util.Collections.<java.lang.String>emptyList()
```

### $N for Names

Generated code is often self-referential. Use **`$N`** to refer to another generated declaration by
Expand Down
24 changes: 23 additions & 1 deletion src/main/java/com/squareup/javapoet/CodeBlock.java
Expand Up @@ -17,10 +17,14 @@

import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

import static com.squareup.javapoet.Util.checkArgument;
Expand All @@ -46,7 +50,12 @@
* that. For example, {@code 6" sandwich} is emitted {@code "6\" sandwich"}.
* <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}.
* type mirrors} and {@linkplain javax.lang.model.element.Element elements}.
* <li>{@code $R} emits a <em>member reference</em>. Arguments for member references may be
* {@linkplain Enum enum constants}, {@linkplain java.lang.reflect.Field fields},
* {@linkplain java.lang.reflect.Method methods},
* {@linkplain javax.lang.model.element.ExecutableElement executable} and
* {@linkplain javax.lang.model.element.VariableElement variable} elements.
* <li>{@code $$} emits a dollar sign.
* <li>{@code $&gt;} increases the indentation level.
* <li>{@code $&lt;} decreases the indentation level.
Expand Down Expand Up @@ -175,6 +184,9 @@ public Builder add(String format, Object... args) {
case 'T':
this.args.add(argToType(args[index]));
break;
case 'R':
this.args.add(argToRef(args[index]));
break;
default:
throw new IllegalArgumentException(
String.format("invalid format string: '%s'", format));
Expand Down Expand Up @@ -225,6 +237,16 @@ private TypeName argToType(Object o) {
throw new IllegalArgumentException("expected type but was " + o);
}

private MemberRef argToRef(Object o) {
if (o instanceof MemberRef) return (MemberRef) o;
if (o instanceof Enum) return MemberRef.get((Enum<?>) o);
if (o instanceof Field) return MemberRef.get((Field) o);
if (o instanceof Method) return MemberRef.get((Method) o);
if (o instanceof ExecutableElement) return MemberRef.get((ExecutableElement) o);
if (o instanceof VariableElement) return MemberRef.get((VariableElement) o);
throw new IllegalArgumentException("expected referable member but was " + o);
}

/**
* @param controlFlow the control flow construct and its code, such as "if (foo == 5)".
* Shouldn't contain braces or newline characters.
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/com/squareup/javapoet/CodeWriter.java
Expand Up @@ -253,6 +253,17 @@ public CodeWriter emit(CodeBlock codeBlock) throws IOException {
typeName.emit(this);
break;

case "$R":
MemberRef memberRef = (MemberRef) codeBlock.args.get(a++);
if (memberRef.isStatic) {
if (isImportedStatically(memberRef.type.canonicalName, memberRef.name)) {
emitAndIndent(memberRef.name);
break;
}
}
memberRef.emit(this);
break;

case "$$":
emitAndIndent("$");
break;
Expand Down Expand Up @@ -308,14 +319,18 @@ private static String extractMemberName(String part) {
return part;
}

private boolean isImportedStatically(String canonicalTypeName, String memberName) {
String explicit = canonicalTypeName + "." + memberName;
String wildcard = canonicalTypeName + ".*";
return staticImports.contains(explicit) || staticImports.contains(wildcard);
}

private boolean emitStaticImportMember(String canonical, String part) throws IOException {
String partWithoutLeadingDot = part.substring(1);
if (partWithoutLeadingDot.isEmpty()) return false;
char first = partWithoutLeadingDot.charAt(0);
if (!Character.isJavaIdentifierStart(first)) return false;
String explicit = canonical + "." + extractMemberName(partWithoutLeadingDot);
String wildcard = canonical + ".*";
if (staticImports.contains(explicit) || staticImports.contains(wildcard)) {
if (isImportedStatically(canonical, extractMemberName(partWithoutLeadingDot))) {
emitAndIndent(partWithoutLeadingDot);
return true;
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/squareup/javapoet/JavaFile.java
Expand Up @@ -238,6 +238,14 @@ public Builder addStaticImport(Class<?> clazz, String... names) {
return addStaticImport(ClassName.get(clazz), names);
}

public Builder addStaticImport(MemberRef... refs) {
for (MemberRef ref : refs) {
checkArgument(ref.isStatic, "only static member references allowed, got %s", ref);
addStaticImport(ref.type, ref.name);
}
return this;
}

public Builder addStaticImport(ClassName className, String... names) {
checkArgument(className != null, "className == null");
checkArgument(names != null, "names == null");
Expand Down
169 changes: 169 additions & 0 deletions src/main/java/com/squareup/javapoet/MemberRef.java
@@ -0,0 +1,169 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* 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
*
* http://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 com.squareup.javapoet;

import static java.lang.reflect.Modifier.isStatic;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

/**
* Member reference class.
*
* Use {@code $R} in format strings to insert member references into your generated code.
*
* @author Christian Stein
*/
public final class MemberRef {
/**
* Defines all well-known member flavours, like {@code ENUM}, {@code FIELD} and {@code METHOD},
* that can be refered to.
*/
public enum Kind {
ENUM, FIELD, METHOD
}

/** Simple getter using JavaPoet-model types only. */
public static MemberRef get(Kind kind, ClassName type, String name, boolean statik,
TypeName... typeArguments) {
Util.checkNotNull(kind, "kind == null");
Util.checkNotNull(type, "type == null");
Util.checkNotNull(name, "name == null");
Util.checkNotNull(typeArguments, "typeArguments == null");
if (kind != Kind.METHOD) {
Util.checkArgument(typeArguments.length == 0, "MemberRef %s mustn't have type args!", kind);
}
return new MemberRef(kind, type, name, statik, Arrays.asList(typeArguments));
}

public static MemberRef get(Enum<?> constant) {
Util.checkNotNull(constant, "constant == null");
ClassName type = ClassName.get(constant.getDeclaringClass());
String name = constant.name();
return get(Kind.ENUM, type, name, true);
}

public static MemberRef get(Field field) {
Util.checkNotNull(field, "field == null");
ClassName type = ClassName.get(field.getDeclaringClass());
String name = field.getName();
boolean statik = isStatic(field.getModifiers());
return get(Kind.FIELD, type, name, statik);
}

public static MemberRef get(Method method, Type... types) {
Util.checkNotNull(method, "method == null");
Util.checkNotNull(types, "types == null");
ClassName type = ClassName.get(method.getDeclaringClass());
String name = method.getName();
boolean statik = isStatic(method.getModifiers());
return get(Kind.METHOD, type, name, statik, TypeName.list(types).toArray(new TypeName[0]));
}

public static MemberRef get(VariableElement variable) {
Util.checkNotNull(variable, "variable == null");
ClassName type = ClassName.get((TypeElement) variable.getEnclosingElement());
String name = variable.getSimpleName().toString();
boolean statik = variable.getModifiers().contains(Modifier.STATIC);
if (variable.getKind() == ElementKind.ENUM_CONSTANT) return get(Kind.FIELD, type, name, statik);
if (variable.getKind() == ElementKind.FIELD) return get(Kind.FIELD, type, name, statik);
throw new IllegalArgumentException("unsupported element kind: " + variable.getKind());
}

public static MemberRef get(ExecutableElement executable, TypeMirror... types) {
Util.checkNotNull(executable, "executable == null");
Util.checkNotNull(types, "types == null");
ClassName type = ClassName.get((TypeElement) executable.getEnclosingElement());
String name = executable.getSimpleName().toString();
boolean statik = executable.getModifiers().contains(Modifier.STATIC);
return get(Kind.METHOD, type, name, statik, TypeName.list(types).toArray(new TypeName[0]));
}

public final Kind kind;
public final ClassName type;
public final String name;
public final boolean isStatic;
public final List<TypeName> typeArguments;

MemberRef(Kind kind, ClassName type, String name, boolean statik) {
this(kind, type, name, statik, Collections.<TypeName>emptyList());
}

MemberRef(Kind kind, ClassName type, String name, boolean statik, List<TypeName> typeArguments) {
this.kind = kind;
this.type = type;
this.name = name;
this.isStatic = statik;
this.typeArguments = Util.immutableList(typeArguments);
}

@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (getClass() != o.getClass()) return false;
MemberRef r = (MemberRef) o;
if (isStatic != r.isStatic) return false;
if (!kind.equals(r.kind)) return false;
if (!type.equals(r.type)) return false;
if (!typeArguments.equals(r.typeArguments)) return false;
return name.equals(r.name);
}

@Override public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (isStatic ? 1731 : 1233);
result = prime * result + kind.hashCode();
result = prime * result + name.hashCode();
result = prime * result + type.hashCode();
result = prime * result + typeArguments.hashCode();
return result;
}

@Override public String toString() {
return type.canonicalName + "." + name;
}

void emit(CodeWriter codeWriter) throws IOException {
if (isStatic) {
codeWriter.emit("$T.", type);
}
if (kind == Kind.METHOD) {
emitTypeArguments(codeWriter);
}
codeWriter.emit("$L", name);
}

void emitTypeArguments(CodeWriter codeWriter) throws IOException {
if (typeArguments.isEmpty()) return;
codeWriter.emit("<");
codeWriter.emit("$T", typeArguments.get(0));
for (int index = 1; index < typeArguments.size(); index++) {
codeWriter.emit(", $T", typeArguments.get(index));
}
codeWriter.emit(">");
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/squareup/javapoet/TypeName.java
Expand Up @@ -363,6 +363,15 @@ static List<TypeName> list(Type[] types, Map<Type, TypeVariableName> map) {
return result;
}

/** Converts an array of type mirrors to a list of type names. */
static List<TypeName> list(TypeMirror[] types) {
List<TypeName> result = new ArrayList<>(types.length);
for (TypeMirror type : types) {
result.add(get(type));
}
return result;
}

/** Returns the array component of {@code type}, or null if {@code type} is not an array. */
static TypeName arrayComponent(TypeName type) {
return type instanceof ArrayTypeName
Expand Down