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