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