blob: 33e38460093c5fd049aa19c05aebbb086da3c170 [file] [log] [blame]
/*
* 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 java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
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.StreamSupport;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import static com.squareup.javapoet.Util.checkArgument;
/**
* A fragment of a .java file, potentially containing declarations, statements, and documentation.
* Code blocks are not necessarily well-formed Java code, and are not validated. This class assumes
* javac will check correctness later!
*
* <p>Code blocks support placeholders like {@link java.text.Format}. Where {@link String#format}
* uses percent {@code %} to reference target values, this class uses dollar sign {@code $} and has
* its own set of permitted placeholders:
*
* <ul>
* <li>{@code $L} emits a <em>literal</em> value with no escaping. Arguments for literals may be
* strings, primitives, {@linkplain TypeSpec type declarations}, {@linkplain AnnotationSpec
* annotations} and even other code blocks.
* <li>{@code $N} emits a <em>name</em>, using name collision avoidance where necessary. Arguments
* for names may be strings (actually any {@linkplain CharSequence character sequence}),
* {@linkplain ParameterSpec parameters}, {@linkplain FieldSpec fields}, {@linkplain
* MethodSpec methods}, and {@linkplain TypeSpec types}.
* <li>{@code $S} escapes the value as a <em>string</em>, wraps it with double quotes, and emits
* 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}.
* <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.
* <li>{@code $Z} acts as a zero-width space. This prefers to wrap lines before 100 columns.
* <li>{@code $>} increases the indentation level.
* <li>{@code $<} decreases the indentation level.
* <li>{@code $[} begins a statement. For multiline statements, every line after the first line
* is double-indented.
* <li>{@code $]} ends a statement.
* </ul>
*/
public final class CodeBlock {
private static final Pattern NAMED_ARGUMENT =
Pattern.compile("\\$(?<argumentName>[\\w_]+):(?<typeChar>[\\w]).*");
private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*");
/** A heterogeneous list containing string literals and value placeholders. */
final List<String> formatParts;
final List<Object> args;
private CodeBlock(Builder builder) {
this.formatParts = Util.immutableList(builder.formatParts);
this.args = Util.immutableList(builder.args);
}
public boolean isEmpty() {
return formatParts.isEmpty();
}
@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 {
new CodeWriter(out).emit(this);
return out.toString();
} catch (IOException e) {
throw new AssertionError();
}
}
public static CodeBlock of(String format, Object... args) {
return new Builder().add(format, args).build();
}
/**
* Joins {@code codeBlocks} into a single {@link CodeBlock}, each separated by {@code separator}.
* For example, joining {@code String s}, {@code Object o} and {@code int i} using {@code ", "}
* would produce {@code String s, Object o, int i}.
*/
public static CodeBlock join(Iterable<CodeBlock> codeBlocks, String separator) {
return StreamSupport.stream(codeBlocks.spliterator(), false).collect(joining(separator));
}
/**
* A {@link Collector} implementation that joins {@link CodeBlock} instances together into one
* separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and
* {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}.
*/
public static Collector<CodeBlock, ?, CodeBlock> joining(String separator) {
return Collector.of(
() -> new CodeBlockJoiner(separator, builder()),
CodeBlockJoiner::add,
CodeBlockJoiner::merge,
CodeBlockJoiner::join);
}
/**
* A {@link Collector} implementation that joins {@link CodeBlock} instances together into one
* separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and
* {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}.
*/
public static Collector<CodeBlock, ?, CodeBlock> joining(
String separator, String prefix, String suffix) {
Builder builder = builder().add("$N", prefix);
return Collector.of(
() -> new CodeBlockJoiner(separator, builder),
CodeBlockJoiner::add,
CodeBlockJoiner::merge,
joiner -> {
builder.add(CodeBlock.of("$N", suffix));
return joiner.join();
});
}
public static Builder builder() {
return new Builder();
}
public Builder toBuilder() {
Builder builder = new Builder();
builder.formatParts.addAll(formatParts);
builder.args.addAll(args);
return builder;
}
public static final class Builder {
final List<String> formatParts = new ArrayList<>();
final List<Object> args = new ArrayList<>();
private Builder() {
}
public boolean isEmpty() {
return formatParts.isEmpty();
}
/**
* Adds code using named arguments.
*
* <p>Named arguments specify their name after the '$' followed by : and the corresponding type
* character. Argument names consist of characters in {@code a-z, A-Z, 0-9, and _} and must
* start with a lowercase character.
*
* <p>For example, to refer to the type {@link java.lang.Integer} with the argument name {@code
* clazz} use a format string containing {@code $clazz:T} and include the key {@code clazz} with
* value {@code java.lang.Integer.class} in the argument map.
*/
public Builder addNamed(String format, Map<String, ?> arguments) {
int p = 0;
for (String argument : arguments.keySet()) {
checkArgument(LOWERCASE.matcher(argument).matches(),
"argument '%s' must start with a lowercase character", argument);
}
while (p < format.length()) {
int nextP = format.indexOf("$", p);
if (nextP == -1) {
formatParts.add(format.substring(p, format.length()));
break;
}
if (p != nextP) {
formatParts.add(format.substring(p, nextP));
p = nextP;
}
Matcher matcher = null;
int colon = format.indexOf(':', p);
if (colon != -1) {
int endIndex = Math.min(colon + 2, format.length());
matcher = NAMED_ARGUMENT.matcher(format.substring(p, endIndex));
}
if (matcher != null && matcher.lookingAt()) {
String argumentName = matcher.group("argumentName");
checkArgument(arguments.containsKey(argumentName), "Missing named argument for $%s",
argumentName);
char formatChar = matcher.group("typeChar").charAt(0);
addArgument(format, formatChar, arguments.get(argumentName));
formatParts.add("$" + formatChar);
p += matcher.regionEnd();
} else {
checkArgument(p < format.length() - 1, "dangling $ at end");
checkArgument(isNoArgPlaceholder(format.charAt(p + 1)),
"unknown format $%s at %s in '%s'", format.charAt(p + 1), p + 1, format);
formatParts.add(format.substring(p, p + 2));
p += 2;
}
}
return this;
}
/**
* Add code with positional or relative arguments.
*
* <p>Relative arguments map 1:1 with the placeholders in the format string.
*
* <p>Positional arguments use an index after the placeholder to identify which argument index
* to use. For example, for a literal to reference the 3rd argument: "$3L" (1 based index)
*
* <p>Mixing relative and positional arguments in a call to add is invalid and will result in an
* error.
*/
public Builder add(String format, Object... args) {
boolean hasRelative = false;
boolean hasIndexed = false;
int relativeParameterCount = 0;
int[] indexedParameterCount = new int[args.length];
for (int p = 0; p < format.length(); ) {
if (format.charAt(p) != '$') {
int nextP = format.indexOf('$', p + 1);
if (nextP == -1) nextP = format.length();
formatParts.add(format.substring(p, nextP));
p = nextP;
continue;
}
p++; // '$'.
// Consume zero or more digits, leaving 'c' as the first non-digit char after the '$'.
int indexStart = p;
char c;
do {
checkArgument(p < format.length(), "dangling format characters in '%s'", format);
c = format.charAt(p++);
} while (c >= '0' && c <= '9');
int indexEnd = p - 1;
// If 'c' doesn't take an argument, we're done.
if (isNoArgPlaceholder(c)) {
checkArgument(
indexStart == indexEnd, "$$, $>, $<, $[, $], $W, and $Z may not have an index");
formatParts.add("$" + c);
continue;
}
// Find either the indexed argument, or the relative argument. (0-based).
int index;
if (indexStart < indexEnd) {
index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1;
hasIndexed = true;
if (args.length > 0) {
indexedParameterCount[index % args.length]++; // modulo is needed, checked below anyway
}
} else {
index = relativeParameterCount;
hasRelative = true;
relativeParameterCount++;
}
checkArgument(index >= 0 && index < args.length,
"index %d for '%s' not in range (received %s arguments)",
index + 1, format.substring(indexStart - 1, indexEnd + 1), args.length);
checkArgument(!hasIndexed || !hasRelative, "cannot mix indexed and positional parameters");
addArgument(format, c, args[index]);
formatParts.add("$" + c);
}
if (hasRelative) {
checkArgument(relativeParameterCount >= args.length,
"unused arguments: expected %s, received %s", relativeParameterCount, args.length);
}
if (hasIndexed) {
List<String> unused = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (indexedParameterCount[i] == 0) {
unused.add("$" + (i + 1));
}
}
String s = unused.size() == 1 ? "" : "s";
checkArgument(unused.isEmpty(), "unused argument%s: %s", s, String.join(", ", unused));
}
return this;
}
private boolean isNoArgPlaceholder(char c) {
return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z';
}
private void addArgument(String format, char c, Object arg) {
switch (c) {
case 'N':
this.args.add(argToName(arg));
break;
case 'L':
this.args.add(argToLiteral(arg));
break;
case 'S':
this.args.add(argToString(arg));
break;
case 'T':
this.args.add(argToType(arg));
break;
default:
throw new IllegalArgumentException(
String.format("invalid format string: '%s'", format));
}
}
private String argToName(Object o) {
if (o instanceof CharSequence) return o.toString();
if (o instanceof ParameterSpec) return ((ParameterSpec) o).name;
if (o instanceof FieldSpec) return ((FieldSpec) o).name;
if (o instanceof MethodSpec) return ((MethodSpec) o).name;
if (o instanceof TypeSpec) return ((TypeSpec) o).name;
throw new IllegalArgumentException("expected name but was " + o);
}
private Object argToLiteral(Object o) {
return o;
}
private String argToString(Object o) {
return o != null ? String.valueOf(o) : null;
}
private TypeName argToType(Object o) {
if (o instanceof TypeName) return (TypeName) o;
if (o instanceof TypeMirror) return TypeName.get((TypeMirror) o);
if (o instanceof Element) return TypeName.get(((Element) o).asType());
if (o instanceof Type) return TypeName.get((Type) o);
throw new IllegalArgumentException("expected type but was " + o);
}
/**
* @param controlFlow the control flow construct and its code, such as "if (foo == 5)".
* Shouldn't contain braces or newline characters.
*/
public Builder beginControlFlow(String controlFlow, Object... args) {
add(controlFlow + " {\n", args);
indent();
return this;
}
/**
* @param controlFlow the control flow construct and its code, such as "else if (foo == 10)".
* Shouldn't contain braces or newline characters.
*/
public Builder nextControlFlow(String controlFlow, Object... args) {
unindent();
add("} " + controlFlow + " {\n", args);
indent();
return this;
}
public Builder endControlFlow() {
unindent();
add("}\n");
return this;
}
/**
* @param controlFlow the optional control flow construct and its code, such as
* "while(foo == 20)". Only used for "do/while" control flows.
*/
public Builder endControlFlow(String controlFlow, Object... args) {
unindent();
add("} " + controlFlow + ";\n", args);
return this;
}
public Builder addStatement(String format, Object... args) {
add("$[");
add(format, args);
add(";\n$]");
return this;
}
public Builder addStatement(CodeBlock codeBlock) {
return addStatement("$L", codeBlock);
}
public Builder add(CodeBlock codeBlock) {
formatParts.addAll(codeBlock.formatParts);
args.addAll(codeBlock.args);
return this;
}
public Builder indent() {
this.formatParts.add("$>");
return this;
}
public Builder unindent() {
this.formatParts.add("$<");
return this;
}
public CodeBlock build() {
return new CodeBlock(this);
}
}
private static final class CodeBlockJoiner {
private final String delimiter;
private final Builder builder;
private boolean first = true;
CodeBlockJoiner(String delimiter, Builder builder) {
this.delimiter = delimiter;
this.builder = builder;
}
CodeBlockJoiner add(CodeBlock codeBlock) {
if (!first) {
builder.add(delimiter);
}
first = false;
builder.add(codeBlock);
return this;
}
CodeBlockJoiner merge(CodeBlockJoiner other) {
CodeBlock otherBlock = other.builder.build();
if (!otherBlock.isEmpty()) {
add(otherBlock);
}
return this;
}
CodeBlock join() {
return builder.build();
}
}
}