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