• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.squareup.javapoet;
17 
18 import java.io.IOException;
19 import java.lang.reflect.Type;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25 import java.util.stream.Collector;
26 import java.util.stream.StreamSupport;
27 import javax.lang.model.element.Element;
28 import javax.lang.model.type.TypeMirror;
29 
30 import static com.squareup.javapoet.Util.checkArgument;
31 
32 /**
33  * A fragment of a .java file, potentially containing declarations, statements, and documentation.
34  * Code blocks are not necessarily well-formed Java code, and are not validated. This class assumes
35  * javac will check correctness later!
36  *
37  * <p>Code blocks support placeholders like {@link java.text.Format}. Where {@link String#format}
38  * uses percent {@code %} to reference target values, this class uses dollar sign {@code $} and has
39  * its own set of permitted placeholders:
40  *
41  * <ul>
42  *   <li>{@code $L} emits a <em>literal</em> value with no escaping. Arguments for literals may be
43  *       strings, primitives, {@linkplain TypeSpec type declarations}, {@linkplain AnnotationSpec
44  *       annotations} and even other code blocks.
45  *   <li>{@code $N} emits a <em>name</em>, using name collision avoidance where necessary. Arguments
46  *       for names may be strings (actually any {@linkplain CharSequence character sequence}),
47  *       {@linkplain ParameterSpec parameters}, {@linkplain FieldSpec fields}, {@linkplain
48  *       MethodSpec methods}, and {@linkplain TypeSpec types}.
49  *   <li>{@code $S} escapes the value as a <em>string</em>, wraps it with double quotes, and emits
50  *       that. For example, {@code 6" sandwich} is emitted {@code "6\" sandwich"}.
51  *   <li>{@code $T} emits a <em>type</em> reference. Types will be imported if possible. Arguments
52  *       for types may be {@linkplain Class classes}, {@linkplain javax.lang.model.type.TypeMirror
53 ,*       type mirrors}, and {@linkplain javax.lang.model.element.Element elements}.
54  *   <li>{@code $$} emits a dollar sign.
55  *   <li>{@code $W} emits a space or a newline, depending on its position on the line. This prefers
56  *       to wrap lines before 100 columns.
57  *   <li>{@code $Z} acts as a zero-width space. This prefers to wrap lines before 100 columns.
58  *   <li>{@code $>} increases the indentation level.
59  *   <li>{@code $<} decreases the indentation level.
60  *   <li>{@code $[} begins a statement. For multiline statements, every line after the first line
61  *       is double-indented.
62  *   <li>{@code $]} ends a statement.
63  * </ul>
64  */
65 public final class CodeBlock {
66   private static final Pattern NAMED_ARGUMENT =
67       Pattern.compile("\\$(?<argumentName>[\\w_]+):(?<typeChar>[\\w]).*");
68   private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*");
69 
70   /** A heterogeneous list containing string literals and value placeholders. */
71   final List<String> formatParts;
72   final List<Object> args;
73 
CodeBlock(Builder builder)74   private CodeBlock(Builder builder) {
75     this.formatParts = Util.immutableList(builder.formatParts);
76     this.args = Util.immutableList(builder.args);
77   }
78 
isEmpty()79   public boolean isEmpty() {
80     return formatParts.isEmpty();
81   }
82 
equals(Object o)83   @Override public boolean equals(Object o) {
84     if (this == o) return true;
85     if (o == null) return false;
86     if (getClass() != o.getClass()) return false;
87     return toString().equals(o.toString());
88   }
89 
hashCode()90   @Override public int hashCode() {
91     return toString().hashCode();
92   }
93 
toString()94   @Override public String toString() {
95     StringBuilder out = new StringBuilder();
96     try {
97       new CodeWriter(out).emit(this);
98       return out.toString();
99     } catch (IOException e) {
100       throw new AssertionError();
101     }
102   }
103 
of(String format, Object... args)104   public static CodeBlock of(String format, Object... args) {
105     return new Builder().add(format, args).build();
106   }
107 
108   /**
109    * Joins {@code codeBlocks} into a single {@link CodeBlock}, each separated by {@code separator}.
110    * For example, joining {@code String s}, {@code Object o} and {@code int i} using {@code ", "}
111    * would produce {@code String s, Object o, int i}.
112    */
join(Iterable<CodeBlock> codeBlocks, String separator)113   public static CodeBlock join(Iterable<CodeBlock> codeBlocks, String separator) {
114     return StreamSupport.stream(codeBlocks.spliterator(), false).collect(joining(separator));
115   }
116 
117   /**
118    * A {@link Collector} implementation that joins {@link CodeBlock} instances together into one
119    * separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and
120    * {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}.
121    */
joining(String separator)122   public static Collector<CodeBlock, ?, CodeBlock> joining(String separator) {
123     return Collector.of(
124         () -> new CodeBlockJoiner(separator, builder()),
125         CodeBlockJoiner::add,
126         CodeBlockJoiner::merge,
127         CodeBlockJoiner::join);
128   }
129 
130   /**
131    * A {@link Collector} implementation that joins {@link CodeBlock} instances together into one
132    * separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and
133    * {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}.
134    */
joining( String separator, String prefix, String suffix)135   public static Collector<CodeBlock, ?, CodeBlock> joining(
136       String separator, String prefix, String suffix) {
137     Builder builder = builder().add("$N", prefix);
138     return Collector.of(
139         () -> new CodeBlockJoiner(separator, builder),
140         CodeBlockJoiner::add,
141         CodeBlockJoiner::merge,
142         joiner -> {
143             builder.add(CodeBlock.of("$N", suffix));
144             return joiner.join();
145         });
146   }
147 
builder()148   public static Builder builder() {
149     return new Builder();
150   }
151 
toBuilder()152   public Builder toBuilder() {
153     Builder builder = new Builder();
154     builder.formatParts.addAll(formatParts);
155     builder.args.addAll(args);
156     return builder;
157   }
158 
159   public static final class Builder {
160     final List<String> formatParts = new ArrayList<>();
161     final List<Object> args = new ArrayList<>();
162 
Builder()163     private Builder() {
164     }
165 
isEmpty()166     public boolean isEmpty() {
167       return formatParts.isEmpty();
168     }
169 
170     /**
171      * Adds code using named arguments.
172      *
173      * <p>Named arguments specify their name after the '$' followed by : and the corresponding type
174      * character. Argument names consist of characters in {@code a-z, A-Z, 0-9, and _} and must
175      * start with a lowercase character.
176      *
177      * <p>For example, to refer to the type {@link java.lang.Integer} with the argument name {@code
178      * clazz} use a format string containing {@code $clazz:T} and include the key {@code clazz} with
179      * value {@code java.lang.Integer.class} in the argument map.
180      */
addNamed(String format, Map<String, ?> arguments)181     public Builder addNamed(String format, Map<String, ?> arguments) {
182       int p = 0;
183 
184       for (String argument : arguments.keySet()) {
185         checkArgument(LOWERCASE.matcher(argument).matches(),
186             "argument '%s' must start with a lowercase character", argument);
187       }
188 
189       while (p < format.length()) {
190         int nextP = format.indexOf("$", p);
191         if (nextP == -1) {
192           formatParts.add(format.substring(p, format.length()));
193           break;
194         }
195 
196         if (p != nextP) {
197           formatParts.add(format.substring(p, nextP));
198           p = nextP;
199         }
200 
201         Matcher matcher = null;
202         int colon = format.indexOf(':', p);
203         if (colon != -1) {
204           int endIndex = Math.min(colon + 2, format.length());
205           matcher = NAMED_ARGUMENT.matcher(format.substring(p, endIndex));
206         }
207         if (matcher != null && matcher.lookingAt()) {
208           String argumentName = matcher.group("argumentName");
209           checkArgument(arguments.containsKey(argumentName), "Missing named argument for $%s",
210               argumentName);
211           char formatChar = matcher.group("typeChar").charAt(0);
212           addArgument(format, formatChar, arguments.get(argumentName));
213           formatParts.add("$" + formatChar);
214           p += matcher.regionEnd();
215         } else {
216           checkArgument(p < format.length() - 1, "dangling $ at end");
217           checkArgument(isNoArgPlaceholder(format.charAt(p + 1)),
218               "unknown format $%s at %s in '%s'", format.charAt(p + 1), p + 1, format);
219           formatParts.add(format.substring(p, p + 2));
220           p += 2;
221         }
222       }
223 
224       return this;
225     }
226 
227     /**
228      * Add code with positional or relative arguments.
229      *
230      * <p>Relative arguments map 1:1 with the placeholders in the format string.
231      *
232      * <p>Positional arguments use an index after the placeholder to identify which argument index
233      * to use. For example, for a literal to reference the 3rd argument: "$3L" (1 based index)
234      *
235      * <p>Mixing relative and positional arguments in a call to add is invalid and will result in an
236      * error.
237      */
238     public Builder add(String format, Object... args) {
239       boolean hasRelative = false;
240       boolean hasIndexed = false;
241 
242       int relativeParameterCount = 0;
243       int[] indexedParameterCount = new int[args.length];
244 
245       for (int p = 0; p < format.length(); ) {
246         if (format.charAt(p) != '$') {
247           int nextP = format.indexOf('$', p + 1);
248           if (nextP == -1) nextP = format.length();
249           formatParts.add(format.substring(p, nextP));
250           p = nextP;
251           continue;
252         }
253 
254         p++; // '$'.
255 
256         // Consume zero or more digits, leaving 'c' as the first non-digit char after the '$'.
257         int indexStart = p;
258         char c;
259         do {
260           checkArgument(p < format.length(), "dangling format characters in '%s'", format);
261           c = format.charAt(p++);
262         } while (c >= '0' && c <= '9');
263         int indexEnd = p - 1;
264 
265         // If 'c' doesn't take an argument, we're done.
266         if (isNoArgPlaceholder(c)) {
267           checkArgument(
268               indexStart == indexEnd, "$$, $>, $<, $[, $], $W, and $Z may not have an index");
269           formatParts.add("$" + c);
270           continue;
271         }
272 
273         // Find either the indexed argument, or the relative argument. (0-based).
274         int index;
275         if (indexStart < indexEnd) {
276           index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1;
277           hasIndexed = true;
278           if (args.length > 0) {
279             indexedParameterCount[index % args.length]++; // modulo is needed, checked below anyway
280           }
281         } else {
282           index = relativeParameterCount;
283           hasRelative = true;
284           relativeParameterCount++;
285         }
286 
287         checkArgument(index >= 0 && index < args.length,
288             "index %d for '%s' not in range (received %s arguments)",
289             index + 1, format.substring(indexStart - 1, indexEnd + 1), args.length);
290         checkArgument(!hasIndexed || !hasRelative, "cannot mix indexed and positional parameters");
291 
292         addArgument(format, c, args[index]);
293 
294         formatParts.add("$" + c);
295       }
296 
297       if (hasRelative) {
298         checkArgument(relativeParameterCount >= args.length,
299             "unused arguments: expected %s, received %s", relativeParameterCount, args.length);
300       }
301       if (hasIndexed) {
302         List<String> unused = new ArrayList<>();
303         for (int i = 0; i < args.length; i++) {
304           if (indexedParameterCount[i] == 0) {
305             unused.add("$" + (i + 1));
306           }
307         }
308         String s = unused.size() == 1 ? "" : "s";
309         checkArgument(unused.isEmpty(), "unused argument%s: %s", s, String.join(", ", unused));
310       }
311       return this;
312     }
313 
314     private boolean isNoArgPlaceholder(char c) {
315       return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z';
316     }
317 
318     private void addArgument(String format, char c, Object arg) {
319       switch (c) {
320         case 'N':
321           this.args.add(argToName(arg));
322           break;
323         case 'L':
324           this.args.add(argToLiteral(arg));
325           break;
326         case 'S':
327           this.args.add(argToString(arg));
328           break;
329         case 'T':
330           this.args.add(argToType(arg));
331           break;
332         default:
333           throw new IllegalArgumentException(
334               String.format("invalid format string: '%s'", format));
335       }
336     }
337 
338     private String argToName(Object o) {
339       if (o instanceof CharSequence) return o.toString();
340       if (o instanceof ParameterSpec) return ((ParameterSpec) o).name;
341       if (o instanceof FieldSpec) return ((FieldSpec) o).name;
342       if (o instanceof MethodSpec) return ((MethodSpec) o).name;
343       if (o instanceof TypeSpec) return ((TypeSpec) o).name;
344       throw new IllegalArgumentException("expected name but was " + o);
345     }
346 
347     private Object argToLiteral(Object o) {
348       return o;
349     }
350 
351     private String argToString(Object o) {
352       return o != null ? String.valueOf(o) : null;
353     }
354 
355     private TypeName argToType(Object o) {
356       if (o instanceof TypeName) return (TypeName) o;
357       if (o instanceof TypeMirror) return TypeName.get((TypeMirror) o);
358       if (o instanceof Element) return TypeName.get(((Element) o).asType());
359       if (o instanceof Type) return TypeName.get((Type) o);
360       throw new IllegalArgumentException("expected type but was " + o);
361     }
362 
363     /**
364      * @param controlFlow the control flow construct and its code, such as "if (foo == 5)".
365      * Shouldn't contain braces or newline characters.
366      */
367     public Builder beginControlFlow(String controlFlow, Object... args) {
368       add(controlFlow + " {\n", args);
369       indent();
370       return this;
371     }
372 
373     /**
374      * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)".
375      *     Shouldn't contain braces or newline characters.
376      */
377     public Builder nextControlFlow(String controlFlow, Object... args) {
378       unindent();
379       add("} " + controlFlow + " {\n", args);
380       indent();
381       return this;
382     }
383 
384     public Builder endControlFlow() {
385       unindent();
386       add("}\n");
387       return this;
388     }
389 
390     /**
391      * @param controlFlow the optional control flow construct and its code, such as
392      *     "while(foo == 20)". Only used for "do/while" control flows.
393      */
394     public Builder endControlFlow(String controlFlow, Object... args) {
395       unindent();
396       add("} " + controlFlow + ";\n", args);
397       return this;
398     }
399 
400     public Builder addStatement(String format, Object... args) {
401       add("$[");
402       add(format, args);
403       add(";\n$]");
404       return this;
405     }
406 
407     public Builder addStatement(CodeBlock codeBlock) {
408       return addStatement("$L", codeBlock);
409     }
410 
411     public Builder add(CodeBlock codeBlock) {
412       formatParts.addAll(codeBlock.formatParts);
413       args.addAll(codeBlock.args);
414       return this;
415     }
416 
417     public Builder indent() {
418       this.formatParts.add("$>");
419       return this;
420     }
421 
422     public Builder unindent() {
423       this.formatParts.add("$<");
424       return this;
425     }
426 
427     public CodeBlock build() {
428       return new CodeBlock(this);
429     }
430   }
431 
432   private static final class CodeBlockJoiner {
433     private final String delimiter;
434     private final Builder builder;
435     private boolean first = true;
436 
437     CodeBlockJoiner(String delimiter, Builder builder) {
438       this.delimiter = delimiter;
439       this.builder = builder;
440     }
441 
442     CodeBlockJoiner add(CodeBlock codeBlock) {
443       if (!first) {
444         builder.add(delimiter);
445       }
446       first = false;
447 
448       builder.add(codeBlock);
449       return this;
450     }
451 
452     CodeBlockJoiner merge(CodeBlockJoiner other) {
453       CodeBlock otherBlock = other.builder.build();
454       if (!otherBlock.isEmpty()) {
455         add(otherBlock);
456       }
457       return this;
458     }
459 
460     CodeBlock join() {
461       return builder.build();
462     }
463   }
464 }
465