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