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.util.ArrayList; 20 import java.util.Collections; 21 import java.util.EnumSet; 22 import java.util.LinkedHashMap; 23 import java.util.LinkedHashSet; 24 import java.util.List; 25 import java.util.ListIterator; 26 import java.util.Locale; 27 import java.util.Map; 28 import java.util.Objects; 29 import java.util.Set; 30 import javax.lang.model.SourceVersion; 31 import javax.lang.model.element.Modifier; 32 33 import static com.squareup.javapoet.Util.checkArgument; 34 import static com.squareup.javapoet.Util.checkNotNull; 35 import static com.squareup.javapoet.Util.checkState; 36 import static com.squareup.javapoet.Util.stringLiteralWithDoubleQuotes; 37 import static java.lang.String.join; 38 39 /** 40 * Converts a {@link JavaFile} to a string suitable to both human- and javac-consumption. This 41 * honors imports, indentation, and deferred variable names. 42 */ 43 final class CodeWriter { 44 /** Sentinel value that indicates that no user-provided package has been set. */ 45 private static final String NO_PACKAGE = new String(); 46 47 private final String indent; 48 private final LineWrapper out; 49 private int indentLevel; 50 51 private boolean javadoc = false; 52 private boolean comment = false; 53 private String packageName = NO_PACKAGE; 54 private final List<TypeSpec> typeSpecStack = new ArrayList<>(); 55 private final Set<String> staticImportClassNames; 56 private final Set<String> staticImports; 57 private final Map<String, ClassName> importedTypes; 58 private final Map<String, ClassName> importableTypes = new LinkedHashMap<>(); 59 private final Set<String> referencedNames = new LinkedHashSet<>(); 60 private boolean trailingNewline; 61 62 /** 63 * When emitting a statement, this is the line of the statement currently being written. The first 64 * line of a statement is indented normally and subsequent wrapped lines are double-indented. This 65 * is -1 when the currently-written line isn't part of a statement. 66 */ 67 int statementLine = -1; 68 CodeWriter(Appendable out)69 CodeWriter(Appendable out) { 70 this(out, " ", Collections.emptySet()); 71 } 72 CodeWriter(Appendable out, String indent, Set<String> staticImports)73 CodeWriter(Appendable out, String indent, Set<String> staticImports) { 74 this(out, indent, Collections.emptyMap(), staticImports); 75 } 76 CodeWriter(Appendable out, String indent, Map<String, ClassName> importedTypes, Set<String> staticImports)77 CodeWriter(Appendable out, String indent, Map<String, ClassName> importedTypes, 78 Set<String> staticImports) { 79 this.out = new LineWrapper(out, indent, 100); 80 this.indent = checkNotNull(indent, "indent == null"); 81 this.importedTypes = checkNotNull(importedTypes, "importedTypes == null"); 82 this.staticImports = checkNotNull(staticImports, "staticImports == null"); 83 this.staticImportClassNames = new LinkedHashSet<>(); 84 for (String signature : staticImports) { 85 staticImportClassNames.add(signature.substring(0, signature.lastIndexOf('.'))); 86 } 87 } 88 importedTypes()89 public Map<String, ClassName> importedTypes() { 90 return importedTypes; 91 } 92 indent()93 public CodeWriter indent() { 94 return indent(1); 95 } 96 indent(int levels)97 public CodeWriter indent(int levels) { 98 indentLevel += levels; 99 return this; 100 } 101 unindent()102 public CodeWriter unindent() { 103 return unindent(1); 104 } 105 unindent(int levels)106 public CodeWriter unindent(int levels) { 107 checkArgument(indentLevel - levels >= 0, "cannot unindent %s from %s", levels, indentLevel); 108 indentLevel -= levels; 109 return this; 110 } 111 pushPackage(String packageName)112 public CodeWriter pushPackage(String packageName) { 113 checkState(this.packageName == NO_PACKAGE, "package already set: %s", this.packageName); 114 this.packageName = checkNotNull(packageName, "packageName == null"); 115 return this; 116 } 117 popPackage()118 public CodeWriter popPackage() { 119 checkState(this.packageName != NO_PACKAGE, "package not set"); 120 this.packageName = NO_PACKAGE; 121 return this; 122 } 123 pushType(TypeSpec type)124 public CodeWriter pushType(TypeSpec type) { 125 this.typeSpecStack.add(type); 126 return this; 127 } 128 popType()129 public CodeWriter popType() { 130 this.typeSpecStack.remove(typeSpecStack.size() - 1); 131 return this; 132 } 133 emitComment(CodeBlock codeBlock)134 public void emitComment(CodeBlock codeBlock) throws IOException { 135 trailingNewline = true; // Force the '//' prefix for the comment. 136 comment = true; 137 try { 138 emit(codeBlock); 139 emit("\n"); 140 } finally { 141 comment = false; 142 } 143 } 144 emitJavadoc(CodeBlock javadocCodeBlock)145 public void emitJavadoc(CodeBlock javadocCodeBlock) throws IOException { 146 if (javadocCodeBlock.isEmpty()) return; 147 148 emit("/**\n"); 149 javadoc = true; 150 try { 151 emit(javadocCodeBlock); 152 } finally { 153 javadoc = false; 154 } 155 emit(" */\n"); 156 } 157 emitAnnotations(List<AnnotationSpec> annotations, boolean inline)158 public void emitAnnotations(List<AnnotationSpec> annotations, boolean inline) throws IOException { 159 for (AnnotationSpec annotationSpec : annotations) { 160 annotationSpec.emit(this, inline); 161 emit(inline ? " " : "\n"); 162 } 163 } 164 165 /** 166 * Emits {@code modifiers} in the standard order. Modifiers in {@code implicitModifiers} will not 167 * be emitted. 168 */ emitModifiers(Set<Modifier> modifiers, Set<Modifier> implicitModifiers)169 public void emitModifiers(Set<Modifier> modifiers, Set<Modifier> implicitModifiers) 170 throws IOException { 171 if (modifiers.isEmpty()) return; 172 for (Modifier modifier : EnumSet.copyOf(modifiers)) { 173 if (implicitModifiers.contains(modifier)) continue; 174 emitAndIndent(modifier.name().toLowerCase(Locale.US)); 175 emitAndIndent(" "); 176 } 177 } 178 emitModifiers(Set<Modifier> modifiers)179 public void emitModifiers(Set<Modifier> modifiers) throws IOException { 180 emitModifiers(modifiers, Collections.emptySet()); 181 } 182 183 /** 184 * Emit type variables with their bounds. This should only be used when declaring type variables; 185 * everywhere else bounds are omitted. 186 */ emitTypeVariables(List<TypeVariableName> typeVariables)187 public void emitTypeVariables(List<TypeVariableName> typeVariables) throws IOException { 188 if (typeVariables.isEmpty()) return; 189 190 emit("<"); 191 boolean firstTypeVariable = true; 192 for (TypeVariableName typeVariable : typeVariables) { 193 if (!firstTypeVariable) emit(", "); 194 emitAnnotations(typeVariable.annotations, true); 195 emit("$L", typeVariable.name); 196 boolean firstBound = true; 197 for (TypeName bound : typeVariable.bounds) { 198 emit(firstBound ? " extends $T" : " & $T", bound); 199 firstBound = false; 200 } 201 firstTypeVariable = false; 202 } 203 emit(">"); 204 } 205 emit(String s)206 public CodeWriter emit(String s) throws IOException { 207 return emitAndIndent(s); 208 } 209 emit(String format, Object... args)210 public CodeWriter emit(String format, Object... args) throws IOException { 211 return emit(CodeBlock.of(format, args)); 212 } 213 emit(CodeBlock codeBlock)214 public CodeWriter emit(CodeBlock codeBlock) throws IOException { 215 int a = 0; 216 ClassName deferredTypeName = null; // used by "import static" logic 217 ListIterator<String> partIterator = codeBlock.formatParts.listIterator(); 218 while (partIterator.hasNext()) { 219 String part = partIterator.next(); 220 switch (part) { 221 case "$L": 222 emitLiteral(codeBlock.args.get(a++)); 223 break; 224 225 case "$N": 226 emitAndIndent((String) codeBlock.args.get(a++)); 227 break; 228 229 case "$S": 230 String string = (String) codeBlock.args.get(a++); 231 // Emit null as a literal null: no quotes. 232 emitAndIndent(string != null 233 ? stringLiteralWithDoubleQuotes(string, indent) 234 : "null"); 235 break; 236 237 case "$T": 238 TypeName typeName = (TypeName) codeBlock.args.get(a++); 239 // defer "typeName.emit(this)" if next format part will be handled by the default case 240 if (typeName instanceof ClassName && partIterator.hasNext()) { 241 if (!codeBlock.formatParts.get(partIterator.nextIndex()).startsWith("$")) { 242 ClassName candidate = (ClassName) typeName; 243 if (staticImportClassNames.contains(candidate.canonicalName)) { 244 checkState(deferredTypeName == null, "pending type for static import?!"); 245 deferredTypeName = candidate; 246 break; 247 } 248 } 249 } 250 typeName.emit(this); 251 break; 252 253 case "$$": 254 emitAndIndent("$"); 255 break; 256 257 case "$>": 258 indent(); 259 break; 260 261 case "$<": 262 unindent(); 263 break; 264 265 case "$[": 266 checkState(statementLine == -1, "statement enter $[ followed by statement enter $["); 267 statementLine = 0; 268 break; 269 270 case "$]": 271 checkState(statementLine != -1, "statement exit $] has no matching statement enter $["); 272 if (statementLine > 0) { 273 unindent(2); // End a multi-line statement. Decrease the indentation level. 274 } 275 statementLine = -1; 276 break; 277 278 case "$W": 279 out.wrappingSpace(indentLevel + 2); 280 break; 281 282 case "$Z": 283 out.zeroWidthSpace(indentLevel + 2); 284 break; 285 286 default: 287 // handle deferred type 288 if (deferredTypeName != null) { 289 if (part.startsWith(".")) { 290 if (emitStaticImportMember(deferredTypeName.canonicalName, part)) { 291 // okay, static import hit and all was emitted, so clean-up and jump to next part 292 deferredTypeName = null; 293 break; 294 } 295 } 296 deferredTypeName.emit(this); 297 deferredTypeName = null; 298 } 299 emitAndIndent(part); 300 break; 301 } 302 } 303 return this; 304 } 305 emitWrappingSpace()306 public CodeWriter emitWrappingSpace() throws IOException { 307 out.wrappingSpace(indentLevel + 2); 308 return this; 309 } 310 extractMemberName(String part)311 private static String extractMemberName(String part) { 312 checkArgument(Character.isJavaIdentifierStart(part.charAt(0)), "not an identifier: %s", part); 313 for (int i = 1; i <= part.length(); i++) { 314 if (!SourceVersion.isIdentifier(part.substring(0, i))) { 315 return part.substring(0, i - 1); 316 } 317 } 318 return part; 319 } 320 emitStaticImportMember(String canonical, String part)321 private boolean emitStaticImportMember(String canonical, String part) throws IOException { 322 String partWithoutLeadingDot = part.substring(1); 323 if (partWithoutLeadingDot.isEmpty()) return false; 324 char first = partWithoutLeadingDot.charAt(0); 325 if (!Character.isJavaIdentifierStart(first)) return false; 326 String explicit = canonical + "." + extractMemberName(partWithoutLeadingDot); 327 String wildcard = canonical + ".*"; 328 if (staticImports.contains(explicit) || staticImports.contains(wildcard)) { 329 emitAndIndent(partWithoutLeadingDot); 330 return true; 331 } 332 return false; 333 } 334 emitLiteral(Object o)335 private void emitLiteral(Object o) throws IOException { 336 if (o instanceof TypeSpec) { 337 TypeSpec typeSpec = (TypeSpec) o; 338 typeSpec.emit(this, null, Collections.emptySet()); 339 } else if (o instanceof AnnotationSpec) { 340 AnnotationSpec annotationSpec = (AnnotationSpec) o; 341 annotationSpec.emit(this, true); 342 } else if (o instanceof CodeBlock) { 343 CodeBlock codeBlock = (CodeBlock) o; 344 emit(codeBlock); 345 } else { 346 emitAndIndent(String.valueOf(o)); 347 } 348 } 349 350 /** 351 * Returns the best name to identify {@code className} with in the current context. This uses the 352 * available imports and the current scope to find the shortest name available. It does not honor 353 * names visible due to inheritance. 354 */ lookupName(ClassName className)355 String lookupName(ClassName className) { 356 // Find the shortest suffix of className that resolves to className. This uses both local type 357 // names (so `Entry` in `Map` refers to `Map.Entry`). Also uses imports. 358 boolean nameResolved = false; 359 for (ClassName c = className; c != null; c = c.enclosingClassName()) { 360 ClassName resolved = resolve(c.simpleName()); 361 nameResolved = resolved != null; 362 363 if (resolved != null && Objects.equals(resolved.canonicalName, c.canonicalName)) { 364 int suffixOffset = c.simpleNames().size() - 1; 365 return join(".", className.simpleNames().subList( 366 suffixOffset, className.simpleNames().size())); 367 } 368 } 369 370 // If the name resolved but wasn't a match, we're stuck with the fully qualified name. 371 if (nameResolved) { 372 return className.canonicalName; 373 } 374 375 // If the class is in the same package, we're done. 376 if (Objects.equals(packageName, className.packageName())) { 377 referencedNames.add(className.topLevelClassName().simpleName()); 378 return join(".", className.simpleNames()); 379 } 380 381 // We'll have to use the fully-qualified name. Mark the type as importable for a future pass. 382 if (!javadoc) { 383 importableType(className); 384 } 385 386 return className.canonicalName; 387 } 388 importableType(ClassName className)389 private void importableType(ClassName className) { 390 if (className.packageName().isEmpty()) { 391 return; 392 } 393 ClassName topLevelClassName = className.topLevelClassName(); 394 String simpleName = topLevelClassName.simpleName(); 395 ClassName replaced = importableTypes.put(simpleName, topLevelClassName); 396 if (replaced != null) { 397 importableTypes.put(simpleName, replaced); // On collision, prefer the first inserted. 398 } 399 } 400 401 /** 402 * Returns the class referenced by {@code simpleName}, using the current nesting context and 403 * imports. 404 */ 405 // TODO(jwilson): also honor superclass members when resolving names. resolve(String simpleName)406 private ClassName resolve(String simpleName) { 407 // Match a child of the current (potentially nested) class. 408 for (int i = typeSpecStack.size() - 1; i >= 0; i--) { 409 TypeSpec typeSpec = typeSpecStack.get(i); 410 for (TypeSpec visibleChild : typeSpec.typeSpecs) { 411 if (Objects.equals(visibleChild.name, simpleName)) { 412 return stackClassName(i, simpleName); 413 } 414 } 415 } 416 417 // Match the top-level class. 418 if (typeSpecStack.size() > 0 && Objects.equals(typeSpecStack.get(0).name, simpleName)) { 419 return ClassName.get(packageName, simpleName); 420 } 421 422 // Match an imported type. 423 ClassName importedType = importedTypes.get(simpleName); 424 if (importedType != null) return importedType; 425 426 // No match. 427 return null; 428 } 429 430 /** Returns the class named {@code simpleName} when nested in the class at {@code stackDepth}. */ stackClassName(int stackDepth, String simpleName)431 private ClassName stackClassName(int stackDepth, String simpleName) { 432 ClassName className = ClassName.get(packageName, typeSpecStack.get(0).name); 433 for (int i = 1; i <= stackDepth; i++) { 434 className = className.nestedClass(typeSpecStack.get(i).name); 435 } 436 return className.nestedClass(simpleName); 437 } 438 439 /** 440 * Emits {@code s} with indentation as required. It's important that all code that writes to 441 * {@link #out} does it through here, since we emit indentation lazily in order to avoid 442 * unnecessary trailing whitespace. 443 */ emitAndIndent(String s)444 CodeWriter emitAndIndent(String s) throws IOException { 445 boolean first = true; 446 for (String line : s.split("\n", -1)) { 447 // Emit a newline character. Make sure blank lines in Javadoc & comments look good. 448 if (!first) { 449 if ((javadoc || comment) && trailingNewline) { 450 emitIndentation(); 451 out.append(javadoc ? " *" : "//"); 452 } 453 out.append("\n"); 454 trailingNewline = true; 455 if (statementLine != -1) { 456 if (statementLine == 0) { 457 indent(2); // Begin multiple-line statement. Increase the indentation level. 458 } 459 statementLine++; 460 } 461 } 462 463 first = false; 464 if (line.isEmpty()) continue; // Don't indent empty lines. 465 466 // Emit indentation and comment prefix if necessary. 467 if (trailingNewline) { 468 emitIndentation(); 469 if (javadoc) { 470 out.append(" * "); 471 } else if (comment) { 472 out.append("// "); 473 } 474 } 475 476 out.append(line); 477 trailingNewline = false; 478 } 479 return this; 480 } 481 emitIndentation()482 private void emitIndentation() throws IOException { 483 for (int j = 0; j < indentLevel; j++) { 484 out.append(indent); 485 } 486 } 487 488 /** 489 * Returns the types that should have been imported for this code. If there were any simple name 490 * collisions, that type's first use is imported. 491 */ suggestedImports()492 Map<String, ClassName> suggestedImports() { 493 Map<String, ClassName> result = new LinkedHashMap<>(importableTypes); 494 result.keySet().removeAll(referencedNames); 495 return result; 496 } 497 } 498