diff --git a/pom.xml b/pom.xml index ff3d422e5..4652697cd 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,30 @@ 4.6.1 test + + junit + junit + 4.13 + compile + + + com.google.guava + guava + 23.4-android + compile + + + com.google.truth + truth + 0.39 + compile + + + com.google.testing.compile + compile-testing + 0.19 + compile + diff --git a/src/main/java/com/squareup/javapoet/AnnotationSpec.java b/src/main/java/com/squareup/javapoet/AnnotationSpec.java index 918c4839e..96b42db22 100644 --- a/src/main/java/com/squareup/javapoet/AnnotationSpec.java +++ b/src/main/java/com/squareup/javapoet/AnnotationSpec.java @@ -19,14 +19,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -247,6 +240,51 @@ public AnnotationSpec build() { } } + /** + * + * @param builder The AnnotationSpec.Builder that you want to add in; + * @param name The name of the AnnotationSpec; + * @param format The format of the @name; + * @param specs The AnnotationSpec that be added in; + * @return the AnnotationSpec.Builder that be added with @specs + */ + public static AnnotationSpec.Builder addMembers(AnnotationSpec.Builder builder, String name, String format, AnnotationSpec... specs) { + final List codeBlocks = new ArrayList<>(); + for (AnnotationSpec spec : specs) { + codeBlocks.add(CodeBlock.of(format, spec)); + } + return addMembers(builder, name, codeBlocks); + } + + /** + * + * @param builder The AnnotationSpec.Builder that you want to add in; + * @param name The name of the AnnotationSpec; + * @param format The format of the @name; + * @param specs The AnnotationSpec that be added in, speciticly, a Collection with AnnotationSpecs; + * @return the AnnotationSpec.Builder that be added with @specs + */ + public static AnnotationSpec.Builder addMembers(AnnotationSpec.Builder builder, String name, String format, Collection specs) { + final List codeBlocks = new ArrayList<>(); + for (AnnotationSpec spec : specs) { + codeBlocks.add(CodeBlock.of(format, spec)); + } + return addMembers(builder, name, codeBlocks); + } + + /** + * + * @param builder The AnnotationSpec.Builder that you want to add in; + * @param name The name of the AnnotationSpec; + * @param codeBlocks The CodeBlocks that be added in, speciticly, a Collection with AnnotationSpecs; + * @return the AnnotationSpec.Builder that be added with @codeBlocks + */ + public static AnnotationSpec.Builder addMembers(AnnotationSpec.Builder builder, String name, Collection codeBlocks) { + final List values = builder.members.computeIfAbsent(name, k -> new ArrayList<>()); + values.addAll(codeBlocks); + return builder; + } + /** * Annotation value visitor adding members to the given builder instance. */ @@ -281,4 +319,5 @@ private static class Visitor extends SimpleAnnotationValueVisitor8{@code $T} emits a type 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}. *
  • {@code $$} emits a dollar sign. *
  • {@code $W} emits a space or a newline, depending on its position on the line. This prefers * to wrap lines before 100 columns. @@ -63,408 +63,431 @@ * */ public final class CodeBlock { - private static final Pattern NAMED_ARGUMENT = - Pattern.compile("\\$(?[\\w_]+):(?[\\w]).*"); - private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*"); - - /** A heterogeneous list containing string literals and value placeholders. */ - final List formatParts; - final List 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 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 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 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 formatParts = new ArrayList<>(); - final List args = new ArrayList<>(); - - private Builder() { - } - - public boolean isEmpty() { - return formatParts.isEmpty(); - } + private static final Pattern NAMED_ARGUMENT = + Pattern.compile("\\$(?[\\w_]+):(?[\\w]).*"); + private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*"); /** - * Adds code using named arguments. - * - *

    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. - * - *

    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. + * A heterogeneous list containing string literals and value placeholders. */ - public Builder addNamed(String format, Map 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)); - break; - } - - if (p != nextP) { - formatParts.add(format.substring(p, nextP)); - p = nextP; - } + final List formatParts; + final List args; - 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; + private CodeBlock(Builder builder) { + this.formatParts = Util.immutableList(builder.formatParts); + this.args = Util.immutableList(builder.args); } - /** - * Add code with positional or relative arguments. - * - *

    Relative arguments map 1:1 with the placeholders in the format string. - * - *

    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) - * - *

    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++; - } + public boolean isEmpty() { + return formatParts.isEmpty(); + } - 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 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; + @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()); } - private boolean isNoArgPlaceholder(char c) { - return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z'; + @Override + public int hashCode() { + return toString().hashCode(); } - 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)); - } + @Override + public String toString() { + StringBuilder out = new StringBuilder(); + try { + new CodeWriter(out).emit(this); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } } - 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); + public static CodeBlock of(String format, Object... args) + { + return new Builder().add(format, args).build(); } - private Object argToLiteral(Object o) { - return o; + public static CodeBlock of(CodeBlock codeBlock) + { + return new Builder().add(codeBlock).build(); } - private String argToString(Object o) { - return o != null ? String.valueOf(o) : null; + public static CodeBlock ofStatement(String format, Object... args) + { + return new Builder().addStatement(format, args).build(); } - 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); + public static CodeBlock ofStatement(CodeBlock codeBlock) + { + return new Builder().addStatement(codeBlock).build(); } + /** - * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". - * Shouldn't contain braces or newline characters. + * 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 Builder beginControlFlow(String controlFlow, Object... args) { - add(controlFlow + " {\n", args); - indent(); - return this; + public static CodeBlock join(Iterable codeBlocks, String separator) { + return StreamSupport.stream(codeBlocks.spliterator(), false).collect(joining(separator)); } /** - * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". - * Shouldn't contain braces or newline characters. + * 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 Builder nextControlFlow(String controlFlow, Object... args) { - unindent(); - add("} " + controlFlow + " {\n", args); - indent(); - return this; - } - - public Builder endControlFlow() { - unindent(); - add("}\n"); - return this; + public static Collector joining(String separator) { + return Collector.of( + () -> new CodeBlockJoiner(separator, builder()), + CodeBlockJoiner::add, + CodeBlockJoiner::merge, + CodeBlockJoiner::join); } /** - * @param controlFlow the optional control flow construct and its code, such as - * "while(foo == 20)". Only used for "do/while" control flows. + * 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 Builder endControlFlow(String controlFlow, Object... args) { - unindent(); - add("} " + controlFlow + ";\n", args); - return this; + public static Collector 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 Builder addStatement(String format, Object... args) { - add("$["); - add(format, args); - add(";\n$]"); - return this; + public static Builder builder() { + return new Builder(); } - public Builder addStatement(CodeBlock codeBlock) { - return addStatement("$L", codeBlock); + public Builder toBuilder() { + Builder builder = new Builder(); + builder.formatParts.addAll(formatParts); + builder.args.addAll(args); + return builder; } - public Builder add(CodeBlock codeBlock) { - formatParts.addAll(codeBlock.formatParts); - args.addAll(codeBlock.args); - return this; - } + public static final class Builder { + final List formatParts = new ArrayList<>(); + final List args = new ArrayList<>(); - public Builder indent() { - this.formatParts.add("$>"); - return this; - } + private Builder() { + } - public Builder unindent() { - this.formatParts.add("$<"); - return this; - } + public boolean isEmpty() { + return formatParts.isEmpty(); + } - public Builder clear() { - formatParts.clear(); - args.clear(); - return this; - } + /** + * Adds code using named arguments. + * + *

    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. + * + *

    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 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)); + 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; + } - public CodeBlock build() { - return new CodeBlock(this); - } - } + /** + * Add code with positional or relative arguments. + * + *

    Relative arguments map 1:1 with the placeholders in the format string. + * + *

    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) + * + *

    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 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 static final class CodeBlockJoiner { - private final String delimiter; - private final Builder builder; - private boolean first = true; + private boolean isNoArgPlaceholder(char c) { + return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z'; + } - CodeBlockJoiner(String delimiter, Builder builder) { - this.delimiter = delimiter; - this.builder = builder; - } + 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)); + } + } - CodeBlockJoiner add(CodeBlock codeBlock) { - if (!first) { - builder.add(delimiter); - } - first = false; + 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); + } - builder.add(codeBlock); - return this; - } + 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); + } - CodeBlockJoiner merge(CodeBlockJoiner other) { - CodeBlock otherBlock = other.builder.build(); - if (!otherBlock.isEmpty()) { - add(otherBlock); - } - return this; + 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 Builder clear() { + formatParts.clear(); + args.clear(); + return this; + } + + public CodeBlock build() { + return new CodeBlock(this); + } } - CodeBlock join() { - return builder.build(); + 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(); + } } - } } diff --git a/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java b/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java index 97c1e6e90..653ba2b2b 100644 --- a/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java +++ b/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java @@ -383,6 +383,85 @@ public class IsAnnotated { assertThat(builder.build().toString()).isEqualTo("@java.lang.SuppressWarnings(\"Bar\")"); } + @Test public void addMembersWithAnnotationSpec(){ + String packageName = "package com.squareup.computerScience;"; + ClassName java = ClassName.get(packageName, "Java"); + ClassName language = ClassName.get(packageName, "Language"); + ClassName computerScience = ClassName.get(packageName, "ComputerScience"); + + AnnotationSpec javaAnnotation = AnnotationSpec.builder(language) + .addMember("name", "$S", "function") + .addMember("language", "$T.class", java) + .build(); + + AnnotationSpec.Builder builder = AnnotationSpec.builder(computerScience); + AnnotationSpec computerScienceAnnotation = AnnotationSpec.addMembers(builder, "ComputerScience", "$L" + , javaAnnotation).build(); + assertThat(computerScienceAnnotation.toString()).isEqualTo( + "@package com.squareup.computerScience;" + + ".ComputerScience" + + "(" + + "ComputerScience = @package com.squareup.computerScience;" + + ".Language" + + "(" + + "name = \"function\", language = package com.squareup.computerScience;" + + ".Java.class" + + ")" + + ")"); + } + + + @Test public void addMembersWithMoreAnnotationSpec(){ + String packageName = "package com.squareup.computerScience;"; + ClassName pytorch = ClassName.get(packageName, "Pytorch"); + ClassName tensorflow = ClassName.get(packageName, "Tensorflow"); + ClassName mxnet = ClassName.get(packageName, "MXNet"); + ClassName dlFramework = ClassName.get(packageName, "DeepLearningFramework"); + ClassName python = ClassName.get(packageName, "Python"); + + AnnotationSpec pytorchAnnotation = AnnotationSpec.builder(dlFramework) + .addMember("name", "$S", "function") + .addMember("DeepLearningFramework", "$T.class", pytorch) + .build(); + + AnnotationSpec tfAnnotation = AnnotationSpec.builder(dlFramework) + .addMember("name", "$S", "function") + .addMember("DeepLearningFramework", "$T.class", tensorflow) + .build(); + + AnnotationSpec mxnetAnnotation = AnnotationSpec.builder(dlFramework) + .addMember("name", "$S", "function") + .addMember("DeepLearningFramework", "$T.class", mxnet) + .build(); + + AnnotationSpec.Builder builder = AnnotationSpec.builder(python); + AnnotationSpec pythonAnnotation = AnnotationSpec.addMembers(builder, "DeepLearningFramework", "$L" + , pytorchAnnotation, tfAnnotation, mxnetAnnotation).build(); + assertThat(pythonAnnotation.toString()).isEqualTo("" + + "@package com.squareup.computerScience;" + + ".Python" + + "(" + + "DeepLearningFramework = " + + "{" + + "@package com.squareup.computerScience;" + + ".DeepLearningFramework" + + "(" + + "name = \"function\", DeepLearningFramework = package com.squareup.computerScience;.Pytorch.class" + + "), " + + "@package com.squareup.computerScience;" + + ".DeepLearningFramework" + + "(" + + "name = \"function\", DeepLearningFramework = package com.squareup.computerScience;.Tensorflow.class" + + "), " + + "@package com.squareup.computerScience;" + + ".DeepLearningFramework" + + "(" + + "name = \"function\", DeepLearningFramework = package com.squareup.computerScience;.MXNet.class" + + ")" + + "}" + + ")\n"); + } + private String toString(TypeSpec typeSpec) { return JavaFile.builder("com.squareup.tacos", typeSpec).build().toString(); } diff --git a/src/test/java/com/squareup/javapoet/CodeBlockTest.java b/src/test/java/com/squareup/javapoet/CodeBlockTest.java index 11b75fa4f..fce24a47b 100644 --- a/src/test/java/com/squareup/javapoet/CodeBlockTest.java +++ b/src/test/java/com/squareup/javapoet/CodeBlockTest.java @@ -19,6 +19,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; + import org.junit.Test; import static com.google.common.truth.Truth.assertThat; @@ -27,325 +28,373 @@ import static org.junit.Assert.fail; public final class CodeBlockTest { - @Test public void equalsAndHashCode() { - CodeBlock a = CodeBlock.builder().build(); - CodeBlock b = CodeBlock.builder().build(); - assertThat(a.equals(b)).isTrue(); - assertThat(a.hashCode()).isEqualTo(b.hashCode()); - a = CodeBlock.builder().add("$L", "taco").build(); - b = CodeBlock.builder().add("$L", "taco").build(); - assertThat(a.equals(b)).isTrue(); - assertThat(a.hashCode()).isEqualTo(b.hashCode()); - } - - @Test public void of() { - CodeBlock a = CodeBlock.of("$L taco", "delicious"); - assertThat(a.toString()).isEqualTo("delicious taco"); - } - - @Test public void isEmpty() { - assertTrue(CodeBlock.builder().isEmpty()); - assertTrue(CodeBlock.builder().add("").isEmpty()); - assertFalse(CodeBlock.builder().add(" ").isEmpty()); - } - - @Test public void indentCannotBeIndexed() { - try { - CodeBlock.builder().add("$1>", "taco").build(); - fail(); - } catch (IllegalArgumentException exp) { - assertThat(exp) - .hasMessageThat() - .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); - } - } - - @Test public void deindentCannotBeIndexed() { - try { - CodeBlock.builder().add("$1<", "taco").build(); - fail(); - } catch (IllegalArgumentException exp) { - assertThat(exp) - .hasMessageThat() - .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); - } - } - - @Test public void dollarSignEscapeCannotBeIndexed() { - try { - CodeBlock.builder().add("$1$", "taco").build(); - fail(); - } catch (IllegalArgumentException exp) { - assertThat(exp) - .hasMessageThat() - .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); - } - } - - @Test public void statementBeginningCannotBeIndexed() { - try { - CodeBlock.builder().add("$1[", "taco").build(); - fail(); - } catch (IllegalArgumentException exp) { - assertThat(exp) - .hasMessageThat() - .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); - } - } - - @Test public void statementEndingCannotBeIndexed() { - try { - CodeBlock.builder().add("$1]", "taco").build(); - fail(); - } catch (IllegalArgumentException exp) { - assertThat(exp) - .hasMessageThat() - .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); - } - } - - @Test public void nameFormatCanBeIndexed() { - CodeBlock block = CodeBlock.builder().add("$1N", "taco").build(); - assertThat(block.toString()).isEqualTo("taco"); - } - - @Test public void literalFormatCanBeIndexed() { - CodeBlock block = CodeBlock.builder().add("$1L", "taco").build(); - assertThat(block.toString()).isEqualTo("taco"); - } - - @Test public void stringFormatCanBeIndexed() { - CodeBlock block = CodeBlock.builder().add("$1S", "taco").build(); - assertThat(block.toString()).isEqualTo("\"taco\""); - } - - @Test public void typeFormatCanBeIndexed() { - CodeBlock block = CodeBlock.builder().add("$1T", String.class).build(); - assertThat(block.toString()).isEqualTo("java.lang.String"); - } - - @Test public void simpleNamedArgument() { - Map map = new LinkedHashMap<>(); - map.put("text", "taco"); - CodeBlock block = CodeBlock.builder().addNamed("$text:S", map).build(); - assertThat(block.toString()).isEqualTo("\"taco\""); - } - - @Test public void repeatedNamedArgument() { - Map map = new LinkedHashMap<>(); - map.put("text", "tacos"); - CodeBlock block = CodeBlock.builder() - .addNamed("\"I like \" + $text:S + \". Do you like \" + $text:S + \"?\"", map) - .build(); - assertThat(block.toString()).isEqualTo( - "\"I like \" + \"tacos\" + \". Do you like \" + \"tacos\" + \"?\""); - } - - @Test public void namedAndNoArgFormat() { - Map map = new LinkedHashMap<>(); - map.put("text", "tacos"); - CodeBlock block = CodeBlock.builder() - .addNamed("$>\n$text:L for $$3.50", map).build(); - assertThat(block.toString()).isEqualTo("\n tacos for $3.50"); - } - - @Test public void missingNamedArgument() { - try { - Map map = new LinkedHashMap<>(); - CodeBlock.builder().addNamed("$text:S", map).build(); - fail(); - } catch(IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Missing named argument for $text"); - } - } - - @Test public void lowerCaseNamed() { - try { - Map map = new LinkedHashMap<>(); - map.put("Text", "tacos"); - CodeBlock block = CodeBlock.builder().addNamed("$Text:S", map).build(); - fail(); - } catch(IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("argument 'Text' must start with a lowercase character"); - } - } - - @Test public void multipleNamedArguments() { - Map map = new LinkedHashMap<>(); - map.put("pipe", System.class); - map.put("text", "tacos"); - - CodeBlock block = CodeBlock.builder() - .addNamed("$pipe:T.out.println(\"Let's eat some $text:L\");", map) - .build(); - - assertThat(block.toString()).isEqualTo( - "java.lang.System.out.println(\"Let's eat some tacos\");"); - } - - @Test public void namedNewline() { - Map map = new LinkedHashMap<>(); - map.put("clazz", Integer.class); - CodeBlock block = CodeBlock.builder().addNamed("$clazz:T\n", map).build(); - assertThat(block.toString()).isEqualTo("java.lang.Integer\n"); - } - - @Test public void danglingNamed() { - Map map = new LinkedHashMap<>(); - map.put("clazz", Integer.class); - try { - CodeBlock.builder().addNamed("$clazz:T$", map).build(); - fail(); - } catch(IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("dangling $ at end"); - } - } - - @Test public void indexTooHigh() { - try { - CodeBlock.builder().add("$2T", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("index 2 for '$2T' not in range (received 1 arguments)"); - } - } - - @Test public void indexIsZero() { - try { - CodeBlock.builder().add("$0T", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("index 0 for '$0T' not in range (received 1 arguments)"); - } - } - - @Test public void indexIsNegative() { - try { - CodeBlock.builder().add("$-1T", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$-1T'"); - } - } - - @Test public void indexWithoutFormatType() { - try { - CodeBlock.builder().add("$1", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("dangling format characters in '$1'"); - } - } - - @Test public void indexWithoutFormatTypeNotAtStringEnd() { - try { - CodeBlock.builder().add("$1 taco", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$1 taco'"); - } - } - - @Test public void indexButNoArguments() { - try { - CodeBlock.builder().add("$1T").build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("index 1 for '$1T' not in range (received 0 arguments)"); - } - } - - @Test public void formatIndicatorAlone() { - try { - CodeBlock.builder().add("$", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("dangling format characters in '$'"); - } - } - - @Test public void formatIndicatorWithoutIndexOrFormatType() { - try { - CodeBlock.builder().add("$ tacoString", String.class).build(); - fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$ tacoString'"); - } - } - - @Test public void sameIndexCanBeUsedWithDifferentFormats() { - CodeBlock block = CodeBlock.builder() - .add("$1T.out.println($1S)", ClassName.get(System.class)) - .build(); - assertThat(block.toString()).isEqualTo("java.lang.System.out.println(\"java.lang.System\")"); - } - - @Test public void tooManyStatementEnters() { - CodeBlock codeBlock = CodeBlock.builder().add("$[$[").build(); - try { - // We can't report this error until rendering type because code blocks might be composed. - codeBlock.toString(); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("statement enter $[ followed by statement enter $["); - } - } - - @Test public void statementExitWithoutStatementEnter() { - CodeBlock codeBlock = CodeBlock.builder().add("$]").build(); - try { - // We can't report this error until rendering type because code blocks might be composed. - codeBlock.toString(); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("statement exit $] has no matching statement enter $["); - } - } - - @Test public void join() { - List codeBlocks = new ArrayList<>(); - codeBlocks.add(CodeBlock.of("$S", "hello")); - codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); - codeBlocks.add(CodeBlock.of("need tacos")); - - CodeBlock joined = CodeBlock.join(codeBlocks, " || "); - assertThat(joined.toString()).isEqualTo("\"hello\" || world.World || need tacos"); - } - - @Test public void joining() { - List codeBlocks = new ArrayList<>(); - codeBlocks.add(CodeBlock.of("$S", "hello")); - codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); - codeBlocks.add(CodeBlock.of("need tacos")); - - CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ")); - assertThat(joined.toString()).isEqualTo("\"hello\" || world.World || need tacos"); - } - - @Test public void joiningSingle() { - List codeBlocks = new ArrayList<>(); - codeBlocks.add(CodeBlock.of("$S", "hello")); - - CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ")); - assertThat(joined.toString()).isEqualTo("\"hello\""); - } - - @Test public void joiningWithPrefixAndSuffix() { - List codeBlocks = new ArrayList<>(); - codeBlocks.add(CodeBlock.of("$S", "hello")); - codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); - codeBlocks.add(CodeBlock.of("need tacos")); - - CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ", "start {", "} end")); - assertThat(joined.toString()).isEqualTo("start {\"hello\" || world.World || need tacos} end"); - } - - @Test public void clear() { - CodeBlock block = CodeBlock.builder() - .addStatement("$S", "Test string") - .clear() - .build(); - - assertThat(block.toString()).isEmpty(); - } + @Test + public void equalsAndHashCode() { + CodeBlock a = CodeBlock.builder().build(); + CodeBlock b = CodeBlock.builder().build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = CodeBlock.builder().add("$L", "taco").build(); + b = CodeBlock.builder().add("$L", "taco").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + public void of() { + CodeBlock a = CodeBlock.of("$L taco", "delicious"); + assertThat(a.toString()).isEqualTo("delicious taco"); + CodeBlock b = CodeBlock.of(a); + assertThat(b.toString()).isEqualTo("delicious taco"); + } + + @Test + public void ofStatement() { + CodeBlock a = CodeBlock.of("$L taco", "delicious"); + assertThat(a.toString()).isEqualTo("delicious taco"); + CodeBlock b = CodeBlock.ofStatement("$L taco", "delicious"); + assertThat(b.toString()).isEqualTo("delicious taco;\n"); + CodeBlock c = CodeBlock.ofStatement(a); + assertThat(c.toString()).isEqualTo("delicious taco;\n"); + } + + @Test + public void isEmpty() { + assertTrue(CodeBlock.builder().isEmpty()); + assertTrue(CodeBlock.builder().add("").isEmpty()); + assertFalse(CodeBlock.builder().add(" ").isEmpty()); + } + + @Test + public void indentCannotBeIndexed() { + try { + CodeBlock.builder().add("$1>", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test + public void deindentCannotBeIndexed() { + try { + CodeBlock.builder().add("$1<", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test + public void dollarSignEscapeCannotBeIndexed() { + try { + CodeBlock.builder().add("$1$", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test + public void statementBeginningCannotBeIndexed() { + try { + CodeBlock.builder().add("$1[", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test + public void statementEndingCannotBeIndexed() { + try { + CodeBlock.builder().add("$1]", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test + public void nameFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1N", "taco").build(); + assertThat(block.toString()).isEqualTo("taco"); + } + + @Test + public void literalFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1L", "taco").build(); + assertThat(block.toString()).isEqualTo("taco"); + } + + @Test + public void stringFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1S", "taco").build(); + assertThat(block.toString()).isEqualTo("\"taco\""); + } + + @Test + public void typeFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1T", String.class).build(); + assertThat(block.toString()).isEqualTo("java.lang.String"); + } + + @Test + public void simpleNamedArgument() { + Map map = new LinkedHashMap<>(); + map.put("text", "taco"); + CodeBlock block = CodeBlock.builder().addNamed("$text:S", map).build(); + assertThat(block.toString()).isEqualTo("\"taco\""); + } + + @Test + public void repeatedNamedArgument() { + Map map = new LinkedHashMap<>(); + map.put("text", "tacos"); + CodeBlock block = CodeBlock.builder() + .addNamed("\"I like \" + $text:S + \". Do you like \" + $text:S + \"?\"", map) + .build(); + assertThat(block.toString()).isEqualTo( + "\"I like \" + \"tacos\" + \". Do you like \" + \"tacos\" + \"?\""); + } + + @Test + public void namedAndNoArgFormat() { + Map map = new LinkedHashMap<>(); + map.put("text", "tacos"); + CodeBlock block = CodeBlock.builder() + .addNamed("$>\n$text:L for $$3.50", map).build(); + assertThat(block.toString()).isEqualTo("\n tacos for $3.50"); + } + + @Test + public void missingNamedArgument() { + try { + Map map = new LinkedHashMap<>(); + CodeBlock.builder().addNamed("$text:S", map).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Missing named argument for $text"); + } + } + + @Test + public void lowerCaseNamed() { + try { + Map map = new LinkedHashMap<>(); + map.put("Text", "tacos"); + CodeBlock block = CodeBlock.builder().addNamed("$Text:S", map).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("argument 'Text' must start with a lowercase character"); + } + } + + @Test + public void multipleNamedArguments() { + Map map = new LinkedHashMap<>(); + map.put("pipe", System.class); + map.put("text", "tacos"); + + CodeBlock block = CodeBlock.builder() + .addNamed("$pipe:T.out.println(\"Let's eat some $text:L\");", map) + .build(); + + assertThat(block.toString()).isEqualTo( + "java.lang.System.out.println(\"Let's eat some tacos\");"); + } + + @Test + public void namedNewline() { + Map map = new LinkedHashMap<>(); + map.put("clazz", Integer.class); + CodeBlock block = CodeBlock.builder().addNamed("$clazz:T\n", map).build(); + assertThat(block.toString()).isEqualTo("java.lang.Integer\n"); + } + + @Test + public void danglingNamed() { + Map map = new LinkedHashMap<>(); + map.put("clazz", Integer.class); + try { + CodeBlock.builder().addNamed("$clazz:T$", map).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("dangling $ at end"); + } + } + + @Test + public void indexTooHigh() { + try { + CodeBlock.builder().add("$2T", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 2 for '$2T' not in range (received 1 arguments)"); + } + } + + @Test + public void indexIsZero() { + try { + CodeBlock.builder().add("$0T", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 0 for '$0T' not in range (received 1 arguments)"); + } + } + + @Test + public void indexIsNegative() { + try { + CodeBlock.builder().add("$-1T", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$-1T'"); + } + } + + @Test + public void indexWithoutFormatType() { + try { + CodeBlock.builder().add("$1", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("dangling format characters in '$1'"); + } + } + + @Test + public void indexWithoutFormatTypeNotAtStringEnd() { + try { + CodeBlock.builder().add("$1 taco", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$1 taco'"); + } + } + + @Test + public void indexButNoArguments() { + try { + CodeBlock.builder().add("$1T").build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 1 for '$1T' not in range (received 0 arguments)"); + } + } + + @Test + public void formatIndicatorAlone() { + try { + CodeBlock.builder().add("$", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("dangling format characters in '$'"); + } + } + + @Test + public void formatIndicatorWithoutIndexOrFormatType() { + try { + CodeBlock.builder().add("$ tacoString", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$ tacoString'"); + } + } + + @Test + public void sameIndexCanBeUsedWithDifferentFormats() { + CodeBlock block = CodeBlock.builder() + .add("$1T.out.println($1S)", ClassName.get(System.class)) + .build(); + assertThat(block.toString()).isEqualTo("java.lang.System.out.println(\"java.lang.System\")"); + } + + @Test + public void tooManyStatementEnters() { + CodeBlock codeBlock = CodeBlock.builder().add("$[$[").build(); + try { + // We can't report this error until rendering type because code blocks might be composed. + codeBlock.toString(); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("statement enter $[ followed by statement enter $["); + } + } + + @Test + public void statementExitWithoutStatementEnter() { + CodeBlock codeBlock = CodeBlock.builder().add("$]").build(); + try { + // We can't report this error until rendering type because code blocks might be composed. + codeBlock.toString(); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("statement exit $] has no matching statement enter $["); + } + } + + @Test + public void join() { + List codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); + codeBlocks.add(CodeBlock.of("need tacos")); + + CodeBlock joined = CodeBlock.join(codeBlocks, " || "); + assertThat(joined.toString()).isEqualTo("\"hello\" || world.World || need tacos"); + } + + @Test + public void joining() { + List codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); + codeBlocks.add(CodeBlock.of("need tacos")); + + CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ")); + assertThat(joined.toString()).isEqualTo("\"hello\" || world.World || need tacos"); + } + + @Test + public void joiningSingle() { + List codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + + CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ")); + assertThat(joined.toString()).isEqualTo("\"hello\""); + } + + @Test + public void joiningWithPrefixAndSuffix() { + List codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); + codeBlocks.add(CodeBlock.of("need tacos")); + + CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ", "start {", "} end")); + assertThat(joined.toString()).isEqualTo("start {\"hello\" || world.World || need tacos} end"); + } + + @Test + public void clear() { + CodeBlock block = CodeBlock.builder() + .addStatement("$S", "Test string") + .clear() + .build(); + + assertThat(block.toString()).isEmpty(); + } }