| /* |
| * Copyright (C) 2015 Square, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.squareup.javapoet; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import javax.lang.model.SourceVersion; |
| import javax.lang.model.element.Modifier; |
| |
| import static com.squareup.javapoet.Util.checkArgument; |
| import static com.squareup.javapoet.Util.checkNotNull; |
| import static com.squareup.javapoet.Util.checkState; |
| import static com.squareup.javapoet.Util.join; |
| import static com.squareup.javapoet.Util.stringLiteralWithDoubleQuotes; |
| |
| /** |
| * Converts a {@link JavaFile} to a string suitable to both human- and javac-consumption. This |
| * honors imports, indentation, and deferred variable names. |
| */ |
| final class CodeWriter { |
| /** Sentinel value that indicates that no user-provided package has been set. */ |
| private static final String NO_PACKAGE = new String(); |
| |
| private final String indent; |
| private final LineWrapper out; |
| private int indentLevel; |
| |
| private boolean javadoc = false; |
| private boolean comment = false; |
| private String packageName = NO_PACKAGE; |
| private final List<TypeSpec> typeSpecStack = new ArrayList<>(); |
| private final Set<String> staticImportClassNames; |
| private final Set<String> staticImports; |
| private final Map<String, ClassName> importedTypes; |
| private final Map<String, ClassName> importableTypes = new LinkedHashMap<>(); |
| private final Set<String> referencedNames = new LinkedHashSet<>(); |
| private boolean trailingNewline; |
| |
| /** |
| * When emitting a statement, this is the line of the statement currently being written. The first |
| * line of a statement is indented normally and subsequent wrapped lines are double-indented. This |
| * is -1 when the currently-written line isn't part of a statement. |
| */ |
| int statementLine = -1; |
| |
| CodeWriter(Appendable out) { |
| this(out, " ", Collections.<String>emptySet()); |
| } |
| |
| CodeWriter(Appendable out, String indent, Set<String> staticImports) { |
| this(out, indent, Collections.<String, ClassName>emptyMap(), staticImports); |
| } |
| |
| CodeWriter(Appendable out, String indent, Map<String, ClassName> importedTypes, |
| Set<String> staticImports) { |
| this.out = new LineWrapper(out, indent, 100); |
| this.indent = checkNotNull(indent, "indent == null"); |
| this.importedTypes = checkNotNull(importedTypes, "importedTypes == null"); |
| this.staticImports = checkNotNull(staticImports, "staticImports == null"); |
| this.staticImportClassNames = new LinkedHashSet<>(); |
| for (String signature : staticImports) { |
| staticImportClassNames.add(signature.substring(0, signature.lastIndexOf('.'))); |
| } |
| } |
| |
| public Map<String, ClassName> importedTypes() { |
| return importedTypes; |
| } |
| |
| public CodeWriter indent() { |
| return indent(1); |
| } |
| |
| public CodeWriter indent(int levels) { |
| indentLevel += levels; |
| return this; |
| } |
| |
| public CodeWriter unindent() { |
| return unindent(1); |
| } |
| |
| public CodeWriter unindent(int levels) { |
| checkArgument(indentLevel - levels >= 0, "cannot unindent %s from %s", levels, indentLevel); |
| indentLevel -= levels; |
| return this; |
| } |
| |
| public CodeWriter pushPackage(String packageName) { |
| checkState(this.packageName == NO_PACKAGE, "package already set: %s", this.packageName); |
| this.packageName = checkNotNull(packageName, "packageName == null"); |
| return this; |
| } |
| |
| public CodeWriter popPackage() { |
| checkState(this.packageName != NO_PACKAGE, "package already set: %s", this.packageName); |
| this.packageName = NO_PACKAGE; |
| return this; |
| } |
| |
| public CodeWriter pushType(TypeSpec type) { |
| this.typeSpecStack.add(type); |
| return this; |
| } |
| |
| public CodeWriter popType() { |
| this.typeSpecStack.remove(typeSpecStack.size() - 1); |
| return this; |
| } |
| |
| public void emitComment(CodeBlock codeBlock) throws IOException { |
| trailingNewline = true; // Force the '//' prefix for the comment. |
| comment = true; |
| try { |
| emit(codeBlock); |
| emit("\n"); |
| } finally { |
| comment = false; |
| } |
| } |
| |
| public void emitJavadoc(CodeBlock javadocCodeBlock) throws IOException { |
| if (javadocCodeBlock.isEmpty()) return; |
| |
| emit("/**\n"); |
| javadoc = true; |
| try { |
| emit(javadocCodeBlock); |
| } finally { |
| javadoc = false; |
| } |
| emit(" */\n"); |
| } |
| |
| public void emitAnnotations(List<AnnotationSpec> annotations, boolean inline) throws IOException { |
| for (AnnotationSpec annotationSpec : annotations) { |
| annotationSpec.emit(this, inline); |
| emit(inline ? " " : "\n"); |
| } |
| } |
| |
| /** |
| * Emits {@code modifiers} in the standard order. Modifiers in {@code implicitModifiers} will not |
| * be emitted. |
| */ |
| public void emitModifiers(Set<Modifier> modifiers, Set<Modifier> implicitModifiers) |
| throws IOException { |
| if (modifiers.isEmpty()) return; |
| for (Modifier modifier : EnumSet.copyOf(modifiers)) { |
| if (implicitModifiers.contains(modifier)) continue; |
| emitAndIndent(modifier.name().toLowerCase(Locale.US)); |
| emitAndIndent(" "); |
| } |
| } |
| |
| public void emitModifiers(Set<Modifier> modifiers) throws IOException { |
| emitModifiers(modifiers, Collections.<Modifier>emptySet()); |
| } |
| |
| /** |
| * Emit type variables with their bounds. This should only be used when declaring type variables; |
| * everywhere else bounds are omitted. |
| */ |
| public void emitTypeVariables(List<TypeVariableName> typeVariables) throws IOException { |
| if (typeVariables.isEmpty()) return; |
| |
| emit("<"); |
| boolean firstTypeVariable = true; |
| for (TypeVariableName typeVariable : typeVariables) { |
| if (!firstTypeVariable) emit(", "); |
| emit("$L", typeVariable.name); |
| boolean firstBound = true; |
| for (TypeName bound : typeVariable.bounds) { |
| emit(firstBound ? " extends $T" : " & $T", bound); |
| firstBound = false; |
| } |
| firstTypeVariable = false; |
| } |
| emit(">"); |
| } |
| |
| public CodeWriter emit(String s) throws IOException { |
| return emitAndIndent(s); |
| } |
| |
| public CodeWriter emit(String format, Object... args) throws IOException { |
| return emit(CodeBlock.of(format, args)); |
| } |
| |
| public CodeWriter emit(CodeBlock codeBlock) throws IOException { |
| int a = 0; |
| ClassName deferredTypeName = null; // used by "import static" logic |
| ListIterator<String> partIterator = codeBlock.formatParts.listIterator(); |
| while (partIterator.hasNext()) { |
| String part = partIterator.next(); |
| switch (part) { |
| case "$L": |
| emitLiteral(codeBlock.args.get(a++)); |
| break; |
| |
| case "$N": |
| emitAndIndent((String) codeBlock.args.get(a++)); |
| break; |
| |
| case "$S": |
| String string = (String) codeBlock.args.get(a++); |
| // Emit null as a literal null: no quotes. |
| emitAndIndent(string != null |
| ? stringLiteralWithDoubleQuotes(string, indent) |
| : "null"); |
| break; |
| |
| case "$T": |
| TypeName typeName = (TypeName) codeBlock.args.get(a++); |
| if (typeName.isAnnotated()) { |
| typeName.emitAnnotations(this); |
| typeName = typeName.withoutAnnotations(); |
| } |
| // defer "typeName.emit(this)" if next format part will be handled by the default case |
| if (typeName instanceof ClassName && partIterator.hasNext()) { |
| if (!codeBlock.formatParts.get(partIterator.nextIndex()).startsWith("$")) { |
| ClassName candidate = (ClassName) typeName; |
| if (staticImportClassNames.contains(candidate.canonicalName)) { |
| checkState(deferredTypeName == null, "pending type for static import?!"); |
| deferredTypeName = candidate; |
| break; |
| } |
| } |
| } |
| typeName.emit(this); |
| break; |
| |
| case "$$": |
| emitAndIndent("$"); |
| break; |
| |
| case "$>": |
| indent(); |
| break; |
| |
| case "$<": |
| unindent(); |
| break; |
| |
| case "$[": |
| checkState(statementLine == -1, "statement enter $[ followed by statement enter $["); |
| statementLine = 0; |
| break; |
| |
| case "$]": |
| checkState(statementLine != -1, "statement exit $] has no matching statement enter $["); |
| if (statementLine > 0) { |
| unindent(2); // End a multi-line statement. Decrease the indentation level. |
| } |
| statementLine = -1; |
| break; |
| |
| case "$W": |
| out.wrappingSpace(indentLevel + 2); |
| break; |
| |
| default: |
| // handle deferred type |
| if (deferredTypeName != null) { |
| if (part.startsWith(".")) { |
| if (emitStaticImportMember(deferredTypeName.canonicalName, part)) { |
| // okay, static import hit and all was emitted, so clean-up and jump to next part |
| deferredTypeName = null; |
| break; |
| } |
| } |
| deferredTypeName.emit(this); |
| deferredTypeName = null; |
| } |
| emitAndIndent(part); |
| break; |
| } |
| } |
| return this; |
| } |
| |
| public CodeWriter emitWrappingSpace() throws IOException { |
| out.wrappingSpace(indentLevel + 2); |
| return this; |
| } |
| |
| private static String extractMemberName(String part) { |
| checkArgument(Character.isJavaIdentifierStart(part.charAt(0)), "not an identifier: %s", part); |
| for (int i = 1; i <= part.length(); i++) { |
| if (!SourceVersion.isIdentifier(part.substring(0, i))) { |
| return part.substring(0, i - 1); |
| } |
| } |
| return part; |
| } |
| |
| private boolean emitStaticImportMember(String canonical, String part) throws IOException { |
| String partWithoutLeadingDot = part.substring(1); |
| if (partWithoutLeadingDot.isEmpty()) return false; |
| char first = partWithoutLeadingDot.charAt(0); |
| if (!Character.isJavaIdentifierStart(first)) return false; |
| String explicit = canonical + "." + extractMemberName(partWithoutLeadingDot); |
| String wildcard = canonical + ".*"; |
| if (staticImports.contains(explicit) || staticImports.contains(wildcard)) { |
| emitAndIndent(partWithoutLeadingDot); |
| return true; |
| } |
| return false; |
| } |
| |
| private void emitLiteral(Object o) throws IOException { |
| if (o instanceof TypeSpec) { |
| TypeSpec typeSpec = (TypeSpec) o; |
| typeSpec.emit(this, null, Collections.<Modifier>emptySet()); |
| } else if (o instanceof AnnotationSpec) { |
| AnnotationSpec annotationSpec = (AnnotationSpec) o; |
| annotationSpec.emit(this, true); |
| } else if (o instanceof CodeBlock) { |
| CodeBlock codeBlock = (CodeBlock) o; |
| emit(codeBlock); |
| } else { |
| emitAndIndent(String.valueOf(o)); |
| } |
| } |
| |
| /** |
| * Returns the best name to identify {@code className} with in the current context. This uses the |
| * available imports and the current scope to find the shortest name available. It does not honor |
| * names visible due to inheritance. |
| */ |
| String lookupName(ClassName className) { |
| // Find the shortest suffix of className that resolves to className. This uses both local type |
| // names (so `Entry` in `Map` refers to `Map.Entry`). Also uses imports. |
| boolean nameResolved = false; |
| for (ClassName c = className; c != null; c = c.enclosingClassName()) { |
| ClassName resolved = resolve(c.simpleName()); |
| nameResolved = resolved != null; |
| |
| if (Objects.equals(resolved, c)) { |
| int suffixOffset = c.simpleNames().size() - 1; |
| return join(".", className.simpleNames().subList( |
| suffixOffset, className.simpleNames().size())); |
| } |
| } |
| |
| // If the name resolved but wasn't a match, we're stuck with the fully qualified name. |
| if (nameResolved) { |
| return className.canonicalName; |
| } |
| |
| // If the class is in the same package, we're done. |
| if (Objects.equals(packageName, className.packageName())) { |
| referencedNames.add(className.topLevelClassName().simpleName()); |
| return join(".", className.simpleNames()); |
| } |
| |
| // We'll have to use the fully-qualified name. Mark the type as importable for a future pass. |
| if (!javadoc) { |
| importableType(className); |
| } |
| |
| return className.canonicalName; |
| } |
| |
| private void importableType(ClassName className) { |
| if (className.packageName().isEmpty()) { |
| return; |
| } |
| ClassName topLevelClassName = className.topLevelClassName(); |
| String simpleName = topLevelClassName.simpleName(); |
| ClassName replaced = importableTypes.put(simpleName, topLevelClassName); |
| if (replaced != null) { |
| importableTypes.put(simpleName, replaced); // On collision, prefer the first inserted. |
| } |
| } |
| |
| /** |
| * Returns the class referenced by {@code simpleName}, using the current nesting context and |
| * imports. |
| */ |
| // TODO(jwilson): also honor superclass members when resolving names. |
| private ClassName resolve(String simpleName) { |
| // Match a child of the current (potentially nested) class. |
| for (int i = typeSpecStack.size() - 1; i >= 0; i--) { |
| TypeSpec typeSpec = typeSpecStack.get(i); |
| for (TypeSpec visibleChild : typeSpec.typeSpecs) { |
| if (Objects.equals(visibleChild.name, simpleName)) { |
| return stackClassName(i, simpleName); |
| } |
| } |
| } |
| |
| // Match the top-level class. |
| if (typeSpecStack.size() > 0 && Objects.equals(typeSpecStack.get(0).name, simpleName)) { |
| return ClassName.get(packageName, simpleName); |
| } |
| |
| // Match an imported type. |
| ClassName importedType = importedTypes.get(simpleName); |
| if (importedType != null) return importedType; |
| |
| // No match. |
| return null; |
| } |
| |
| /** Returns the class named {@code simpleName} when nested in the class at {@code stackDepth}. */ |
| private ClassName stackClassName(int stackDepth, String simpleName) { |
| ClassName className = ClassName.get(packageName, typeSpecStack.get(0).name); |
| for (int i = 1; i <= stackDepth; i++) { |
| className = className.nestedClass(typeSpecStack.get(i).name); |
| } |
| return className.nestedClass(simpleName); |
| } |
| |
| /** |
| * Emits {@code s} with indentation as required. It's important that all code that writes to |
| * {@link #out} does it through here, since we emit indentation lazily in order to avoid |
| * unnecessary trailing whitespace. |
| */ |
| CodeWriter emitAndIndent(String s) throws IOException { |
| boolean first = true; |
| for (String line : s.split("\n", -1)) { |
| // Emit a newline character. Make sure blank lines in Javadoc & comments look good. |
| if (!first) { |
| if ((javadoc || comment) && trailingNewline) { |
| emitIndentation(); |
| out.append(javadoc ? " *" : "//"); |
| } |
| out.append("\n"); |
| trailingNewline = true; |
| if (statementLine != -1) { |
| if (statementLine == 0) { |
| indent(2); // Begin multiple-line statement. Increase the indentation level. |
| } |
| statementLine++; |
| } |
| } |
| |
| first = false; |
| if (line.isEmpty()) continue; // Don't indent empty lines. |
| |
| // Emit indentation and comment prefix if necessary. |
| if (trailingNewline) { |
| emitIndentation(); |
| if (javadoc) { |
| out.append(" * "); |
| } else if (comment) { |
| out.append("// "); |
| } |
| } |
| |
| out.append(line); |
| trailingNewline = false; |
| } |
| return this; |
| } |
| |
| private void emitIndentation() throws IOException { |
| for (int j = 0; j < indentLevel; j++) { |
| out.append(indent); |
| } |
| } |
| |
| /** |
| * Returns the types that should have been imported for this code. If there were any simple name |
| * collisions, that type's first use is imported. |
| */ |
| Map<String, ClassName> suggestedImports() { |
| Map<String, ClassName> result = new LinkedHashMap<>(importableTypes); |
| result.keySet().removeAll(referencedNames); |
| return result; |
| } |
| } |