diff options
author | Ashley Rose <ashleyrose@google.com> | 2018-11-19 16:56:29 -0500 |
---|---|---|
committer | Ashley Rose <ashleyrose@google.com> | 2018-11-19 16:56:29 -0500 |
commit | f4c8cf525f18c05f4e5339b0ea9dba1f825fb5e6 (patch) | |
tree | c45e39951dd760bd55e5d56c9dc1575c3bdeefcc | |
parent | 6803793e9e910afe8eed9b71bce546d3e23a6b89 (diff) | |
parent | a5db06d05f8bd799cef194af28ace07427e2d07e (diff) | |
download | javapoet-f4c8cf525f18c05f4e5339b0ea9dba1f825fb5e6.tar.gz |
Merge commit 'a5db06d'
This is the upstream commit for JavaPoet v1.11.1
Change-Id: I3372f7a59c8888b66930b67e73f8a9d3a6c51167
46 files changed, 12517 insertions, 0 deletions
diff --git a/.buildscript/deploy_snapshot.sh b/.buildscript/deploy_snapshot.sh new file mode 100755 index 0000000..395a873 --- /dev/null +++ b/.buildscript/deploy_snapshot.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. +# +# Adapted from https://coderwall.com/p/9b_lfq and +# https://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ + +SLUG="square/javapoet" +JDK="oraclejdk8" +BRANCH="master" + +set -e + +if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then + echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'." +elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then + echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'." +elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo "Skipping snapshot deployment: was pull request." +elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then + echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'." +else + echo "Deploying snapshot..." + mvn clean source:jar javadoc:jar deploy --settings=".buildscript/settings.xml" -Dmaven.test.skip=true + echo "Snapshot deployed!" +fi diff --git a/.buildscript/settings.xml b/.buildscript/settings.xml new file mode 100644 index 0000000..91f444b --- /dev/null +++ b/.buildscript/settings.xml @@ -0,0 +1,9 @@ +<settings> + <servers> + <server> + <id>sonatype-nexus-snapshots</id> + <username>${env.CI_DEPLOY_USERNAME}</username> + <password>${env.CI_DEPLOY_PASSWORD}</password> + </server> + </servers> +</settings> diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be0d31a --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +.classpath +.project +.settings +.checkstyle +eclipsebin + +bin +gen +build +out +lib + +target +pom.xml.* +release.properties + +.idea +*.iml +classes + +obj + +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4e0a881 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,46 @@ +language: java + +matrix: + include: + - env: JDK='Oracle JDK 8' + jdk: oraclejdk8 + - env: JDK='Oracle JDK 9' + jdk: oraclejdk9 + - env: JDK='Oracle JDK 10' + install: . ./install-jdk.sh -F 10 -L BCL + - env: JDK='OpenJDK 10' + install: . ./install-jdk.sh -F 10 -L GPL + - env: JDK='Oracle JDK 11' + install: . ./install-jdk.sh -F 11 -L BCL + - env: JDK='OpenJDK 11' + install: . ./install-jdk.sh -F 11 -L GPL + allow_failures: + # ErrorProne/javac is not yet working on JDK 11 + - env: JDK='Oracle JDK 11' + - env: JDK='OpenJDK 11' + +# Direct usage of `install-jdk.sh` might be superseded by https://github.com/travis-ci/travis-build/pull/1347 +before_install: + - unset _JAVA_OPTIONS + - wget https://github.com/sormuras/bach/raw/1.0.1/install-jdk.sh + +after_success: + - .buildscript/deploy_snapshot.sh + +env: + global: + - secure: "nkVNCk8H2orIZOmow0t+Qub1lFQCYpJgNZf17zYI5x0JVqQNCqkcTYYDHqzwkvkmixXFCrfYZQuXy7x2qg9zjCX+vmhlmiMWwe8dNa34OLTseuuR2irS0C8nRGRYxKM7EGenRZSqbFVUksKRm2iWnHKxtmCzeDaS7MoMit2wdUo=" + - secure: "j8+hPaZnyM+UlOBYOEA96fPbVWbN6bMQ28SGQnFMwxo2axHi9ww9Au1N7002HzHnxX8iyesdWFBigArnEL8zKEoXH9Bmur0sn3Ys4bu72C3ozscP4cjXfYSHj8aVLp1EIMdQPDF7MkCccx9l7ONdsW0ltmdiVUtDxzqkH+63WLU=" + +branches: + except: + - gh-pages + +notifications: + email: false + +sudo: false + +cache: + directories: + - $HOME/.m2 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3933dd0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,302 @@ +Change Log +========== + +JavaPoet 1.11.1 *(2018-05-16)* +----------------------------- + + * Fix: JavaPoet 1.11 had a regression where `TypeName.get()` would throw on error types, masking + other errors in an annotation processing round. This is fixed with a test to prevent future + regressions! + + +JavaPoet 1.11.0 *(2018-04-29)* +----------------------------- + + * New: Support `TYPE_USE` annotations on each enclosing `ClassName`. + * New: Work around a compiler bug in `TypeName.get(TypeElement)`. There was a problem getting an + element's kind when building from source ABIs. + + +JavaPoet 1.10.0 *(2018-01-27)* +----------------------------- + + * **JavaPoet now requires Java 8 or newer.** + * New: `$Z` as an optional newline (zero-width space) if a line may exceed 100 chars. + * New: `CodeBlock.join()` and `CodeBlock.joining()` let you join codeblocks by delimiters. + * New: Add `CodeBlock.Builder.isEmpty()`. + * New: `addStatement(CodeBlock)` overloads for `CodeBlock` and `MethodSpec`. + * Fix: Include annotations when emitting type variables. + * Fix: Use the right imports for annotated type parameters. + * Fix: Don't incorrectly escape classnames that start with `$`. + + +JavaPoet 1.9.0 *(2017-05-13)* +----------------------------- + + * Fix: Don't emit incorrect code when the declared type's signature references another type with + the same simple name. + * Fix: Support anonymous inner classes in `ClassName.get()`. + * New: `MethodSpec.Builder.addNamedCode()` and `TypeSpec.anonymousClassBuilder(CodeBlock)`. + + +JavaPoet 1.8.0 *(2016-11-09)* +----------------------------- + + * New: Basic support for line wrapping. Use `$W` to insert a Wrappable Whitespace character. It'll + emit either a single space or a newline with appropriate indentation. + * New: Named arguments in `CodeBlock`. These are intended to make larger code snippets easier to + read: + + ``` + Map<String, Object> map = new LinkedHashMap<>(); + map.put("count", 3); + map.put("greeting", "Hello, "); + map.put("system", System.class); + + String template = "" + + "for (int i = 0; i < $count:L; i++) {\n" + + " $system:T.out.println($greeting:S + list.get(i));\n" + + "}\n"; + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.addNamed(template, map); + ``` + + * New: `addJavadoc(CodeBlock)` overloads for TypeSpec, MethodSpec, and FieldSpec. + * New: `MethodSpec.addComment()` makes it easy to add a `// single-line comment.` + * New: `ClassName.getReflectionName()` returns a string like `java.util.Map$Entry`. + * Fix: Always write UTF-8. Previously JavaPoet would use the system default charset which was + potentially inconsistent across environments. + * Fix: Permit (constant) fields to be defined in annotation types. + + +JavaPoet 1.7.0 *(2016-04-26)* +----------------------------- + + * New: Support parameterized types that enclose other types, like `Outer<String>.Inner`. + * New: `TypeName.isBoxedPrimitive()`. + + +JavaPoet 1.6.1 *(2016-03-21)* +----------------------------- + + * Fix: Double quotes and backslashes in string literals were not properly quoted in 1.6.0. This is + now fixed. + + +JavaPoet 1.6.0 *(2016-03-19)* +----------------------------- + + * New: Revive `CodeBlock.of()`, a handy factory method for building code blocks. + * New: Add `TypeSpec` factory methods that take a `ClassName`. + * New: `TypeName.annotated()` adds an annotation to a type. + * New: `TypeVariableName.withBounds()` adds bounds to a type variable. + * New: `TypeSpec.Builder.addInitializerBlock()` adds an instance initializer. + * Fix: Make `TypeSpec.Kind` enum public. This can be used to check if a `TypeSpec` is a class, + interface, enum, or annotation. + * Fix: Don’t break import resolution on annotated types. + * Fix: Forbid unexpected modifiers like `private` on annotation members. + * Fix: Deduplicate exceptions in `MethodSpec.Builder`. + * Fix: Treat `ErrorType` like a regular `DeclaredType` in `TypeName.get()`. This should make it + easier to write annotation processors. + + +JavaPoet 1.5.1 *(2016-01-10)* +----------------------------- + + * Fix: Annotated `TypeName` instances are only equal if their annotations are equal. + +JavaPoet 1.5.0 *(2016-01-10)* +----------------------------- + + * New: `import static`! See `JavaFile.Builder.addStaticImport()` variants. + * New: Overload `NameAllocator.newName(String)` for creating a one-off name without a tag. + * Fix: AnnotationSpec escapes character literals properly. + * Fix: Don't stack overflow when `TypeVariableName` is part of `ParameterizedTypeName`. + * Fix: Reporting not used indexed arguments in like `add("$1S", "a", "b")`. + * Fix: Prevent import of types located in the default package, i.e. have no package name. + + +JavaPoet 1.4.0 *(2015-11-13)* +----------------------------- + + * New: `AnnotationSpec.get(Annotation)`. + * New: Type annotations! `TypeName.annotated()` can attach annotations like `@Nullable` directly to + types. This works for both top-level types and type parameters as in `List<@Nullable String>`. + * New: `equals()` and `hashCode()` on `AnnotationSpec`, `CodeBlock`, `FieldSpec`, `JavaFile`, + `MethodSpec`, `ParameterSpec`, `TypeName`, and `TypeSpec`. + * New: `NameAllocator.clone()` to refine a NameAllocator for use in an inner scope code block. + * Fix: Don't stack overflow when `TypeVariableName` gets a self-referential type. + * Fix: Better handling of name collisions on imports. Previously JavaPoet did the wrong thing when + a referenced type and a nested types had the same name. + + +JavaPoet 1.3.0 *(2015-09-20)* +----------------------------- + + * New: `NameAllocator` API makes it easy to declare non-conflicting names. + * New: Support annotations on enum values. + * Fix: Avoid infinite recursion in `TypeName.get(TypeMirror)`. + * Fix: Use qualified name for conflicting simple names in the same file. + * Fix: Better messages for parameter indexing errors. + + +JavaPoet 1.2.0 *(2015-07-04)* +----------------------------- + + * New: Arguments may have positional indexes like `$1T` and `$2N`. Indexes can be used to refer to + the same argument multiple times in a single format string. + * New: Permit Javadoc on enum constants. + * New: Class initializer blocks with `addStaticBlock()`. + * Fix: `MethodSpec.overriding()` retains annotations. + + +JavaPoet 1.1.0 *(2015-05-25)* +----------------------------- + + * New: Eager validation of argument types like `$T` and `$N`. + * New: `MethodSpec.varargs(boolean)` to generate varags methods. + * New: `AnnotationSpec.get()` and `MethodSpec.overriding()` to create annotations and methods from + the `javax.lang.model` API. + * New: `JavaFile.toJavaFileObject()`. + * New: Java 8 `DEFAULT` modifier. + * New: `toBuilder()` methods to build upon objects already constructed. + * New: Generate `@interface` annotation types. + * New: `TypeName.box()` and `TypeName.unbox()` convenience APIs. + * Fix: `nextControlFlow()` accepts arguments. + * Fix: Reject duplicate calls to set the superclass. + * Fix: `WildcardTypeName.get(WildcardType)` no longer throws a `NullPointerException`. + * Fix: Don't allow double field initialization. + +JavaPoet 1.0.0 *(2015-01-28)* +----------------------------- + + * This update is a complete rewrite. The project name is now `javapoet`. We renamed the it so you + can simultaneously use the old JavaWriter API and our new builder-based APIs in one project. + * Immutable value objects and builders. Instead of streaming the `.java` file from top to bottom, + you now define members in whatever way is convenient. + * We now use our own models for type names. + * Imports are now added automatically. + + +JavaWriter 2.5.1 *(2014-12-03)* +------------------------------- + + * New: `StringLiteral` type which encapsulates the behavior of `stringLiteral`. + * Fix: Use canonical name when emitting a class import. + * Fix: Apply type compression to varargs and array types. + * Fix: Restore binary compatibility with pre-2.5 versions. + + +JavaWriter 2.5.0 *(2014-04-18)* +------------------------------- + + * New: Methods in interfaces will always have no body declaration. + * New: Control flow begin declaration now supports String format arguments. + * Fix: Truncate any generic type when emitting constructors. + * Fix: Do not emit trailing whitespace after '=' at end-of-line. + + +JavaWriter 2.4.0 *(2014-01-10)* +------------------------------- + + * New: Properly indent hanging lines in field initializers. + * New: `emitEnumValue` variant which exposes a boolean of whether the current value is the last. + + +JavaWriter 2.3.1 *(2013-12-16)* +------------------------------- + + * Fix: Properly handle subpackages of `java.lang` in `compressType`. + + +JavaWriter 2.3.0 *(2013-11-24)* +------------------------------- + + * New: Configurable indent level via `setIndent`. + * New: `beginConstructor` method is a semantically clearer alternative for constructors. + * New: `emitEnumValues` method emits a list of values followed by semicolon. + * `emitImports` now supports `Class` arguments directly. + * Previously-deprecated, `int`-based modifier methods have been removed. + + +JavaWriter 2.2.1 *(2013-10-23)* +------------------------------- + + * Fix: Do not emit trailing whitespace for empty Javadoc lines. + + +JavaWriter 2.2.0 *(2013-09-25)* +------------------------------- + + * `setCompressingTypes` controls whether types are emitted as fully-qualified or not. + + +JavaWriter 2.1.2 *(2013-08-23)* +------------------------------- + + * Attempt to keep annotations on a single line. + + +JavaWriter 2.1.1 *(2013-07-23)* +------------------------------- + + * Fix: `stringLiteral` now correctly handles escapes and control characters. + + +JavaWriter 2.1.0 *(2013-07-15)* +------------------------------- + + * New: All methods now take a `Set` of `Modifier`s rather than an `int`. The `int` methods are + now deprecated for removal in JavaPoet 1.0. + * Annotations with a single "value" attribute will now omit the key. + + +JavaWriter 2.0.1 *(2013-06-17)* +------------------------------- + + * Correct casing of `emitSingleLineComment`. + + +JavaWriter 2.0.0 *(2013-06-06)* +------------------------------- + + * Package name is now `com.squareup.javawriter`. + * Support declaring `throws` clause on methods. + + +JavaWriter 1.0.5 *(2013-05-08)* +------------------------------- + + * Fix: Fully qualify types whose simple name matches an import. + + +JavaWriter 1.0.4 *(2013-03-15)* +------------------------------- + + * Fix: Static import emit now properly supports method imports. + + +JavaWriter 1.0.3 *(2013-02-21)* +------------------------------- + + * Add support for emitting static imports. + + +JavaWriter 1.0.2 *(2013-02-11)* +------------------------------- + + * Add `type` API for helping build generic types. + * Minor performance improvements. + + +JavaWriter 1.0.1 *(2013-02-03)* +------------------------------- + + * Expose `compressType` API. + + +JavaWriter 1.0.0 *(2013-02-01)* +------------------------------- + +Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8131805 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +Contributing +============ + +If you would like to contribute code you can do so through GitHub by forking +the repository and sending a pull request. + +When submitting code, please make every effort to follow existing conventions +and style in order to keep the code as readable as possible. Please also make +sure your code compiles by running `mvn clean verify`. Checkstyle failures +during compilation indicate errors in your style and can be viewed in the +`checkstyle-result.xml` file. + +Before your code can be accepted into the project you must also sign the +[Individual Contributor License Agreement (CLA)][1]. + + + [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dffbad --- /dev/null +++ b/README.md @@ -0,0 +1,889 @@ +JavaPoet +======== + +`JavaPoet` is a Java API for generating `.java` source files. + +Source file generation can be useful when doing things such as annotation processing or interacting +with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate +the need to write boilerplate while also keeping a single source of truth for the metadata. + + +### Example + +Here's a (boring) `HelloWorld` class: + +```java +package com.example.helloworld; + +public final class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, JavaPoet!"); + } +} +``` + +And this is the (exciting) code to generate it with JavaPoet: + +```java +MethodSpec main = MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(void.class) + .addParameter(String[].class, "args") + .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") + .build(); + +TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethod(main) + .build(); + +JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) + .build(); + +javaFile.writeTo(System.out); +``` + +To declare the main method, we've created a `MethodSpec` "main" configured with modifiers, return +type, parameters and code statements. We add the main method to a `HelloWorld` class, and then add +that to a `HelloWorld.java` file. + +In this case we write the file to `System.out`, but we could also get it as a string +(`JavaFile.toString()`) or write it to the file system (`JavaFile.writeTo()`). + +The [Javadoc][javadoc] catalogs the complete JavaPoet API, which we explore below. + +### Code & Control Flow + +Most of JavaPoet's API uses plain old immutable Java objects. There's also builders, method chaining +and varargs to make the API friendly. JavaPoet offers models for classes & interfaces (`TypeSpec`), +fields (`FieldSpec`), methods & constructors (`MethodSpec`), parameters (`ParameterSpec`) and +annotations (`AnnotationSpec`). + +But the _body_ of methods and constructors is not modeled. There's no expression class, no +statement class or syntax tree nodes. Instead, JavaPoet uses strings for code blocks: + +```java +MethodSpec main = MethodSpec.methodBuilder("main") + .addCode("" + + "int total = 0;\n" + + "for (int i = 0; i < 10; i++) {\n" + + " total += i;\n" + + "}\n") + .build(); +``` + +Which generates this: + +```java +void main() { + int total = 0; + for (int i = 0; i < 10; i++) { + total += i; + } +} +``` + +The manual semicolons, line wrapping, and indentation are tedious and so JavaPoet offers APIs to +make it easier. There's `addStatement()` which takes care of semicolons and newline, and +`beginControlFlow()` + `endControlFlow()` which are used together for braces, newlines, and +indentation: + +```java +MethodSpec main = MethodSpec.methodBuilder("main") + .addStatement("int total = 0") + .beginControlFlow("for (int i = 0; i < 10; i++)") + .addStatement("total += i") + .endControlFlow() + .build(); +``` + +This example is lame because the generated code is constant! Suppose instead of just adding 0 to 10, +we want to make the operation and range configurable. Here's a method that generates a method: + +```java +private MethodSpec computeRange(String name, int from, int to, String op) { + return MethodSpec.methodBuilder(name) + .returns(int.class) + .addStatement("int result = 1") + .beginControlFlow("for (int i = " + from + "; i < " + to + "; i++)") + .addStatement("result = result " + op + " i") + .endControlFlow() + .addStatement("return result") + .build(); +} +``` + +And here's what we get when we call `computeRange("multiply10to20", 10, 20, "*")`: + +```java +int multiply10to20() { + int result = 1; + for (int i = 10; i < 20; i++) { + result = result * i; + } + return result; +} +``` + +Methods generating methods! And since JavaPoet generates source instead of bytecode, you can +read through it to make sure it's right. + + +### $L for Literals + +The string-concatenation in calls to `beginControlFlow()` and `addStatement` is distracting. Too +many operators. To address this, JavaPoet offers a syntax inspired-by but incompatible-with +[`String.format()`][formatter]. It accepts **`$L`** to emit a **literal** value in the output. This +works just like `Formatter`'s `%s`: + +```java +private MethodSpec computeRange(String name, int from, int to, String op) { + return MethodSpec.methodBuilder(name) + .returns(int.class) + .addStatement("int result = 0") + .beginControlFlow("for (int i = $L; i < $L; i++)", from, to) + .addStatement("result = result $L i", op) + .endControlFlow() + .addStatement("return result") + .build(); +} +``` + +Literals are emitted directly to the output code with no escaping. Arguments for literals may be +strings, primitives, and a few JavaPoet types described below. + +### $S for Strings + +When emitting code that includes string literals, we can use **`$S`** to emit a **string**, complete +with wrapping quotation marks and escaping. Here's a program that emits 3 methods, each of which +returns its own name: + +```java +public static void main(String[] args) throws Exception { + TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethod(whatsMyName("slimShady")) + .addMethod(whatsMyName("eminem")) + .addMethod(whatsMyName("marshallMathers")) + .build(); + + JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) + .build(); + + javaFile.writeTo(System.out); +} + +private static MethodSpec whatsMyName(String name) { + return MethodSpec.methodBuilder(name) + .returns(String.class) + .addStatement("return $S", name) + .build(); +} +``` + +In this case, using `$S` gives us quotation marks: + +```java +public final class HelloWorld { + String slimShady() { + return "slimShady"; + } + + String eminem() { + return "eminem"; + } + + String marshallMathers() { + return "marshallMathers"; + } +} +``` + +### $T for Types + +We Java programmers love our types: they make our code easier to understand. And JavaPoet is on +board. It has rich built-in support for types, including automatic generation of `import` +statements. Just use **`$T`** to reference **types**: + +```java +MethodSpec today = MethodSpec.methodBuilder("today") + .returns(Date.class) + .addStatement("return new $T()", Date.class) + .build(); + +TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethod(today) + .build(); + +JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) + .build(); + +javaFile.writeTo(System.out); +``` + +That generates the following `.java` file, complete with the necessary `import`: + +```java +package com.example.helloworld; + +import java.util.Date; + +public final class HelloWorld { + Date today() { + return new Date(); + } +} +``` + +We passed `Date.class` to reference a class that just-so-happens to be available when we're +generating code. This doesn't need to be the case. Here's a similar example, but this one +references a class that doesn't exist (yet): + +```java +ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard"); + +MethodSpec today = MethodSpec.methodBuilder("tomorrow") + .returns(hoverboard) + .addStatement("return new $T()", hoverboard) + .build(); +``` + +And that not-yet-existent class is imported as well: + +```java +package com.example.helloworld; + +import com.mattel.Hoverboard; + +public final class HelloWorld { + Hoverboard tomorrow() { + return new Hoverboard(); + } +} +``` + +The `ClassName` type is very important, and you'll need it frequently when you're using JavaPoet. +It can identify any _declared_ class. Declared types are just the beginning of Java's rich type +system: we also have arrays, parameterized types, wildcard types, and type variables. JavaPoet has +classes for building each of these: + +```java +ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard"); +ClassName list = ClassName.get("java.util", "List"); +ClassName arrayList = ClassName.get("java.util", "ArrayList"); +TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard); + +MethodSpec beyond = MethodSpec.methodBuilder("beyond") + .returns(listOfHoverboards) + .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList) + .addStatement("result.add(new $T())", hoverboard) + .addStatement("result.add(new $T())", hoverboard) + .addStatement("result.add(new $T())", hoverboard) + .addStatement("return result") + .build(); +``` + +JavaPoet will decompose each type and import its components where possible. + +```java +package com.example.helloworld; + +import com.mattel.Hoverboard; +import java.util.ArrayList; +import java.util.List; + +public final class HelloWorld { + List<Hoverboard> beyond() { + List<Hoverboard> result = new ArrayList<>(); + result.add(new Hoverboard()); + result.add(new Hoverboard()); + result.add(new Hoverboard()); + return result; + } +} +``` + +#### Import static + +JavaPoet supports `import static`. It does it via explicitly collecting type member names. Let's +enhance the previous example with some static sugar: + +```java +... +ClassName namedBoards = ClassName.get("com.mattel", "Hoverboard", "Boards"); + +MethodSpec beyond = MethodSpec.methodBuilder("beyond") + .returns(listOfHoverboards) + .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList) + .addStatement("result.add($T.createNimbus(2000))", hoverboard) + .addStatement("result.add($T.createNimbus(\"2001\"))", hoverboard) + .addStatement("result.add($T.createNimbus($T.THUNDERBOLT))", hoverboard, namedBoards) + .addStatement("$T.sort(result)", Collections.class) + .addStatement("return result.isEmpty() ? $T.emptyList() : result", Collections.class) + .build(); + +TypeSpec hello = TypeSpec.classBuilder("HelloWorld") + .addMethod(beyond) + .build(); + +JavaFile.builder("com.example.helloworld", hello) + .addStaticImport(hoverboard, "createNimbus") + .addStaticImport(namedBoards, "*") + .addStaticImport(Collections.class, "*") + .build(); +``` + +JavaPoet will first add your `import static` block to the file as configured, match and mangle +all calls accordingly and also import all other types as needed. + +```java +package com.example.helloworld; + +import static com.mattel.Hoverboard.Boards.*; +import static com.mattel.Hoverboard.createNimbus; +import static java.util.Collections.*; + +import com.mattel.Hoverboard; +import java.util.ArrayList; +import java.util.List; + +class HelloWorld { + List<Hoverboard> beyond() { + List<Hoverboard> result = new ArrayList<>(); + result.add(createNimbus(2000)); + result.add(createNimbus("2001")); + result.add(createNimbus(THUNDERBOLT)); + sort(result); + return result.isEmpty() ? emptyList() : result; + } +} +``` + +### $N for Names + +Generated code is often self-referential. Use **`$N`** to refer to another generated declaration by +its name. Here's a method that calls another: + +```java +public String byteToHex(int b) { + char[] result = new char[2]; + result[0] = hexDigit((b >>> 4) & 0xf); + result[1] = hexDigit(b & 0xf); + return new String(result); +} + +public char hexDigit(int i) { + return (char) (i < 10 ? i + '0' : i - 10 + 'a'); +} +``` + +When generating the code above, we pass the `hexDigit()` method as an argument to the `byteToHex()` +method using `$N`: + +```java +MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit") + .addParameter(int.class, "i") + .returns(char.class) + .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')") + .build(); + +MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex") + .addParameter(int.class, "b") + .returns(String.class) + .addStatement("char[] result = new char[2]") + .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit) + .addStatement("result[1] = $N(b & 0xf)", hexDigit) + .addStatement("return new String(result)") + .build(); +``` + +### Code block format strings + +Code blocks may specify the values for their placeholders in a few ways. Only one style may be used +for each operation on a code block. + +#### Relative Arguments + +Pass an argument value for each placeholder in the format string to `CodeBlock.add()`. In each +example, we generate code to say "I ate 3 tacos" + +```java +CodeBlock.builder().add("I ate $L $L", 3, "tacos") +``` + +#### Positional Arguments + +Place an integer index (1-based) before the placeholder in the format string to specify which + argument to use. + +```java +CodeBlock.builder().add("I ate $2L $1L", "tacos", 3) +``` + +#### Named Arguments + +Use the syntax `$argumentName:X` where `X` is the format character and call `CodeBlock.addNamed()` +with a map containing all argument keys in the format string. Argument names use characters in +`a-z`, `A-Z`, `0-9`, and `_`, and must start with a lowercase character. + +```java +Map<String, Object> map = new LinkedHashMap<>(); +map.put("food", "tacos"); +map.put("count", 3); +CodeBlock.builder().addNamed("I ate $count:L $food:L", map) +``` + +### Methods + +All of the above methods have a code body. Use `Modifiers.ABSTRACT` to get a method without any +body. This is only legal if the enclosing class is either abstract or an interface. + +```java +MethodSpec flux = MethodSpec.methodBuilder("flux") + .addModifiers(Modifier.ABSTRACT, Modifier.PROTECTED) + .build(); + +TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addMethod(flux) + .build(); +``` + +Which generates this: + +```java +public abstract class HelloWorld { + protected abstract void flux(); +} +``` + +The other modifiers work where permitted. Note that when specifying modifiers, JavaPoet uses +[`javax.lang.model.element.Modifier`][modifier], a class that is not available on Android. This +limitation applies to code-generating-code only; the output code runs everywhere: JVMs, Android, +and GWT. + +Methods also have parameters, exceptions, varargs, Javadoc, annotations, type variables, and a +return type. All of these are configured with `MethodSpec.Builder`. + +### Constructors + +`MethodSpec` is a slight misnomer; it can also be used for constructors: + +```java +MethodSpec flux = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(String.class, "greeting") + .addStatement("this.$N = $N", "greeting", "greeting") + .build(); + +TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC) + .addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL) + .addMethod(flux) + .build(); +``` + +Which generates this: + +```java +public class HelloWorld { + private final String greeting; + + public HelloWorld(String greeting) { + this.greeting = greeting; + } +} +``` + +For the most part, constructors work just like methods. When emitting code, JavaPoet will place +constructors before methods in the output file. + +### Parameters + +Declare parameters on methods and constructors with either `ParameterSpec.builder()` or +`MethodSpec`'s convenient `addParameter()` API: + +```java +ParameterSpec android = ParameterSpec.builder(String.class, "android") + .addModifiers(Modifier.FINAL) + .build(); + +MethodSpec welcomeOverlords = MethodSpec.methodBuilder("welcomeOverlords") + .addParameter(android) + .addParameter(String.class, "robot", Modifier.FINAL) + .build(); +``` + +Though the code above to generate `android` and `robot` parameters is different, the output is the +same: + +```java +void welcomeOverlords(final String android, final String robot) { +} +``` + +The extended `Builder` form is necessary when the parameter has annotations (such as `@Nullable`). + +### Fields + +Like parameters, fields can be created either with builders or by using convenient helper methods: + +```java +FieldSpec android = FieldSpec.builder(String.class, "android") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build(); + +TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC) + .addField(android) + .addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL) + .build(); +``` + +Which generates: + +```java +public class HelloWorld { + private final String android; + + private final String robot; +} +``` + +The extended `Builder` form is necessary when a field has Javadoc, annotations, or a field +initializer. Field initializers use the same [`String.format()`][formatter]-like syntax as the code +blocks above: + +```java +FieldSpec android = FieldSpec.builder(String.class, "android") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer("$S + $L", "Lollipop v.", 5.0d) + .build(); +``` + +Which generates: + +```java +private final String android = "Lollipop v." + 5.0; +``` + +### Interfaces + +JavaPoet has no trouble with interfaces. Note that interface methods must always be `PUBLIC +ABSTRACT` and interface fields must always be `PUBLIC STATIC FINAL`. These modifiers are necessary +when defining the interface: + +```java +TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC) + .addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("$S", "change") + .build()) + .addMethod(MethodSpec.methodBuilder("beep") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .build()) + .build(); +``` + +But these modifiers are omitted when the code is generated. These are the defaults so we don't need +to include them for `javac`'s benefit! + +```java +public interface HelloWorld { + String ONLY_THING_THAT_IS_CONSTANT = "change"; + + void beep(); +} +``` + +### Enums + +Use `enumBuilder` to create the enum type, and `addEnumConstant()` for each value: + +```java +TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo") + .addModifiers(Modifier.PUBLIC) + .addEnumConstant("ROCK") + .addEnumConstant("SCISSORS") + .addEnumConstant("PAPER") + .build(); +``` + +To generate this: + +```java +public enum Roshambo { + ROCK, + + SCISSORS, + + PAPER +} +``` + +Fancy enums are supported, where the enum values override methods or call a superclass constructor. +Here's a comprehensive example: + +```java +TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo") + .addModifiers(Modifier.PUBLIC) + .addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("$S", "fist") + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addStatement("return $S", "avalanche!") + .returns(String.class) + .build()) + .build()) + .addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace") + .build()) + .addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat") + .build()) + .addField(String.class, "handsign", Modifier.PRIVATE, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(String.class, "handsign") + .addStatement("this.$N = $N", "handsign", "handsign") + .build()) + .build(); +``` + +Which generates this: + +```java +public enum Roshambo { + ROCK("fist") { + @Override + public String toString() { + return "avalanche!"; + } + }, + + SCISSORS("peace"), + + PAPER("flat"); + + private final String handsign; + + Roshambo(String handsign) { + this.handsign = handsign; + } +} +``` + +### Anonymous Inner Classes + +In the enum code, we used `Types.anonymousInnerClass()`. Anonymous inner classes can also be used in +code blocks. They are values that can be referenced with `$L`: + +```java +TypeSpec comparator = TypeSpec.anonymousClassBuilder("") + .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class)) + .addMethod(MethodSpec.methodBuilder("compare") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(String.class, "a") + .addParameter(String.class, "b") + .returns(int.class) + .addStatement("return $N.length() - $N.length()", "a", "b") + .build()) + .build(); + +TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addMethod(MethodSpec.methodBuilder("sortByLength") + .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings") + .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator) + .build()) + .build(); +``` + +This generates a method that contains a class that contains a method: + +```java +void sortByLength(List<String> strings) { + Collections.sort(strings, new Comparator<String>() { + @Override + public int compare(String a, String b) { + return a.length() - b.length(); + } + }); +} +``` + +One particularly tricky part of defining anonymous inner classes is the arguments to the superclass +constructor. In the above code we're passing the empty string for no arguments: +`TypeSpec.anonymousClassBuilder("")`. To pass different parameters use JavaPoet's code block +syntax with commas to separate arguments. + + +### Annotations + +Simple annotations are easy: + +```java +MethodSpec toString = MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .returns(String.class) + .addModifiers(Modifier.PUBLIC) + .addStatement("return $S", "Hoverboard") + .build(); +``` + +Which generates this method with an `@Override` annotation: + +```java + @Override + public String toString() { + return "Hoverboard"; + } +``` + +Use `AnnotationSpec.builder()` to set properties on annotations: + +```java +MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addAnnotation(AnnotationSpec.builder(Headers.class) + .addMember("accept", "$S", "application/json; charset=utf-8") + .addMember("userAgent", "$S", "Square Cash") + .build()) + .addParameter(LogRecord.class, "logRecord") + .returns(LogReceipt.class) + .build(); +``` + +Which generates this annotation with `accept` and `userAgent` properties: + +```java +@Headers( + accept = "application/json; charset=utf-8", + userAgent = "Square Cash" +) +LogReceipt recordEvent(LogRecord logRecord); +``` + +When you get fancy, annotation values can be annotations themselves. Use `$L` for embedded +annotations: + +```java +MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addAnnotation(AnnotationSpec.builder(HeaderList.class) + .addMember("value", "$L", AnnotationSpec.builder(Header.class) + .addMember("name", "$S", "Accept") + .addMember("value", "$S", "application/json; charset=utf-8") + .build()) + .addMember("value", "$L", AnnotationSpec.builder(Header.class) + .addMember("name", "$S", "User-Agent") + .addMember("value", "$S", "Square Cash") + .build()) + .build()) + .addParameter(LogRecord.class, "logRecord") + .returns(LogReceipt.class) + .build(); +``` + +Which generates this: + +```java +@HeaderList({ + @Header(name = "Accept", value = "application/json; charset=utf-8"), + @Header(name = "User-Agent", value = "Square Cash") +}) +LogReceipt recordEvent(LogRecord logRecord); +``` + +Note that you can call `addMember()` multiple times with the same property name to populate a list +of values for that property. + +### Javadoc + +Fields, methods and types can be documented with Javadoc: + +```java +MethodSpec dismiss = MethodSpec.methodBuilder("dismiss") + .addJavadoc("Hides {@code message} from the caller's history. Other\n" + + "participants in the conversation will continue to see the\n" + + "message in their own history unless they also delete it.\n") + .addJavadoc("\n") + .addJavadoc("<p>Use {@link #delete($T)} to delete the entire\n" + + "conversation for all participants.\n", Conversation.class) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameter(Message.class, "message") + .build(); +``` + +Which generates this: + +```java + /** + * Hides {@code message} from the caller's history. Other + * participants in the conversation will continue to see the + * message in their own history unless they also delete it. + * + * <p>Use {@link #delete(Conversation)} to delete the entire + * conversation for all participants. + */ + void dismiss(Message message); +``` + +Use `$T` when referencing types in Javadoc to get automatic imports. + +Download +-------- + +Download [the latest .jar][dl] or depend via Maven: +```xml +<dependency> + <groupId>com.squareup</groupId> + <artifactId>javapoet</artifactId> + <version>1.11.1</version> +</dependency> +``` +or Gradle: +```groovy +compile 'com.squareup:javapoet:1.11.1' +``` + +Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. + + + +License +------- + + Copyright 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. + + + +JavaWriter +========== + +JavaPoet is the successor to [JavaWriter][javawriter]. New projects should prefer JavaPoet because +it has a stronger code model: it understands types and can manage imports automatically. JavaPoet is +also better suited to composition: rather than streaming the contents of a `.java` file +top-to-bottom in a single pass, a file can be assembled as a tree of declarations. + +JavaWriter continues to be available in [GitHub][javawriter] and [Maven Central][javawriter_maven]. + + + [dl]: https://search.maven.org/remote_content?g=com.squareup&a=javapoet&v=LATEST + [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/javapoet/ + [javadoc]: https://square.github.io/javapoet/1.x/javapoet/ + [javawriter]: https://github.com/square/javapoet/tree/javawriter_2 + [javawriter_maven]: https://search.maven.org/#artifactdetails%7Ccom.squareup%7Cjavawriter%7C2.5.1%7Cjar + [formatter]: https://developer.android.com/reference/java/util/Formatter.html + [modifier]: https://docs.oracle.com/javase/8/docs/api/javax/lang/model/element/Modifier.html diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..bbf690e --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,148 @@ +<?xml version="1.0"?> +<!DOCTYPE module PUBLIC + "-//Puppy Crawl//DTD Check Configuration 1.3//EN" + "http://www.puppycrawl.com/dtds/configuration_1_3.dtd"> + +<module name="Checker"> + <module name="SuppressWarningsFilter"/> + <module name="NewlineAtEndOfFile"/> + <module name="FileLength"/> + <module name="FileTabCharacter"/> + + <!-- Trailing spaces --> + <module name="RegexpSingleline"> + <property name="format" value="\s+$"/> + <property name="message" value="Line has trailing spaces."/> + </module> + + <!-- Space after 'for' and 'if' --> + <module name="RegexpSingleline"> + <property name="format" value="^\s*(for|if)\b[^ ]"/> + <property name="message" value="Space needed before opening parenthesis."/> + </module> + + <!-- For each spacing --> + <module name="RegexpSingleline"> + <property name="format" value="^\s*for \(.*?([^ ]:|:[^ ])"/> + <property name="message" value="Space needed around ':' character."/> + </module> + + <module name="TreeWalker"> + <property name="cacheFile" value="${checkstyle.cache.file}"/> + + <!-- Checks for Javadoc comments. --> + <!-- See http://checkstyle.sf.net/config_javadoc.html --> + <!--module name="JavadocMethod"/--> + <!--module name="JavadocType"/--> + <!--module name="JavadocVariable"/--> + <module name="JavadocStyle"/> + + + <!-- Checks for Naming Conventions. --> + <!-- See http://checkstyle.sf.net/config_naming.html --> + <module name="ConstantName"/> + <module name="LocalFinalVariableName"/> + <module name="LocalVariableName"/> + <module name="MemberName"/> + <module name="MethodName"/> + <module name="PackageName"/> + <module name="ParameterName"/> + <module name="StaticVariableName"/> + <module name="TypeName"/> + + + <!-- Checks for imports --> + <!-- See http://checkstyle.sf.net/config_import.html --> + <module name="AvoidStarImport"/> + <module name="IllegalImport"/> + <!-- defaults to sun.* packages --> + <module name="RedundantImport"/> + <module name="UnusedImports"> + <property name="processJavadoc" value="true"/> + </module> + + + <!-- Checks for Size Violations. --> + <!-- See http://checkstyle.sf.net/config_sizes.html --> + <module name="LineLength"> + <property name="max" value="100"/> + </module> + <module name="MethodLength"> + <property name="max" value="160"/> + </module> + <module name="ParameterNumber"/> + + + <!-- Checks for whitespace --> + <!-- See http://checkstyle.sf.net/config_whitespace.html --> + <module name="GenericWhitespace"/> + <!--<module name="EmptyForIteratorPad"/>--> + <module name="MethodParamPad"/> + <module name="NoWhitespaceAfter"/> + <module name="NoWhitespaceBefore"/> + <module name="OperatorWrap"/> + <module name="ParenPad"/> + <module name="TypecastParenPad"/> + <module name="WhitespaceAfter"/> + <module name="WhitespaceAround"> + <property name="tokens" + value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, + COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_CATCH, + LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, + LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, + MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, SL, SLIST, + SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/> + </module> + + + <!-- Modifier Checks --> + <!-- See http://checkstyle.sf.net/config_modifiers.html --> + <module name="ModifierOrder"/> + <module name="RedundantModifier"/> + + + <!-- Checks for blocks. You know, those {}'s --> + <!-- See http://checkstyle.sf.net/config_blocks.html --> + <module name="AvoidNestedBlocks"/> + <!--module name="EmptyBlock"/--> + <module name="LeftCurly"/> + <!--<module name="NeedBraces"/>--> + <module name="RightCurly"/> + + + <!-- Checks for common coding problems --> + <!-- See http://checkstyle.sf.net/config_coding.html --> + <!--module name="AvoidInlineConditionals"/--> + <module name="CovariantEquals"/> + <module name="EmptyStatement"/> + <!--<module name="EqualsAvoidNull"/>--> + <module name="EqualsHashCode"/> + <!--module name="HiddenField"/--> + <module name="IllegalInstantiation"/> + <module name="InnerAssignment"/> + <!--<module name="MagicNumber"/>--> + <module name="MissingSwitchDefault"/> + <!--module name="RedundantThrows"/--> + <module name="SimplifyBooleanExpression"/> + <module name="SimplifyBooleanReturn"/> + + <!-- Checks for class design --> + <!-- See http://checkstyle.sf.net/config_design.html --> + <!--module name="DesignForExtension"/--> + <module name="FinalClass"/> + <module name="HideUtilityClassConstructor"/> + <module name="InterfaceIsType"/> + <!--module name="VisibilityModifier"/--> + + + <!-- Miscellaneous other checks. --> + <!-- See http://checkstyle.sf.net/config_misc.html --> + <module name="ArrayTypeStyle"/> + <!--module name="FinalParameters"/--> + <module name="TodoComment"/> + <module name="UpperEll"/> + + <!-- Make the @SuppressWarnings annotations available to Checkstyle --> + <module name="SuppressWarningsHolder"/> + </module> +</module> @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.sonatype.oss</groupId> + <artifactId>oss-parent</artifactId> + <version>7</version> + </parent> + + <groupId>com.squareup</groupId> + <artifactId>javapoet</artifactId> + <version>1.11.1</version> + + <name>JavaPoet</name> + <description>Use beautiful Java code to generate beautiful Java code.</description> + <url>http://github.com/square/javapoet/</url> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + + <java.version>1.8</java.version> + <junit.version>4.12</junit.version> + <truth.version>0.39</truth.version> + <compile-testing.version>0.15</compile-testing.version> + </properties> + + <scm> + <url>http://github.com/square/javapoet/</url> + <connection>scm:git:git://github.com/square/javapoet.git</connection> + <developerConnection>scm:git:ssh://git@github.com/square/javapoet.git</developerConnection> + <tag>HEAD</tag> + </scm> + + <issueManagement> + <system>GitHub Issues</system> + <url>http://github.com/square/javapoet/issues</url> + </issueManagement> + + <licenses> + <license> + <name>Apache 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> + </license> + </licenses> + + <organization> + <name>Square, Inc.</name> + <url>http://squareup.com</url> + </organization> + + <dependencies> + <dependency> + <groupId>com.google.truth</groupId> + <artifactId>truth</artifactId> + <version>${truth.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.testing.compile</groupId> + <artifactId>compile-testing</artifactId> + <version>${compile-testing.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.jimfs</groupId> + <artifactId>jimfs</artifactId> + <version>1.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>2.13.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jdt.core.compiler</groupId> + <artifactId>ecj</artifactId> + <version>4.6.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.7.0</version> + <configuration> + <compilerId>javac-with-errorprone</compilerId> + <forceJavacCompilerUse>true</forceJavacCompilerUse> + <source>${java.version}</source> + <target>${java.version}</target> + </configuration> + <dependencies> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-compiler-javac-errorprone</artifactId> + <version>2.8.2</version> + </dependency> + <dependency> + <groupId>com.google.errorprone</groupId> + <artifactId>error_prone_core</artifactId> + <version>2.3.1</version> + </dependency> + </dependencies> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>2.17</version> + <dependencies> + <dependency> + <groupId>com.puppycrawl.tools</groupId> + <artifactId>checkstyle</artifactId> + <version>8.7</version> + </dependency> + </dependencies> + <configuration> + <failsOnError>true</failsOnError> + <configLocation>checkstyle.xml</configLocation> + <consoleOutput>true</consoleOutput> + </configuration> + <executions> + <execution> + <phase>verify</phase> + <goals> + <goal>checkstyle</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.0.2</version> + <configuration> + <archive> + <manifestEntries> + <Automatic-Module-Name>com.squareup.javapoet</Automatic-Module-Name> + </manifestEntries> + </archive> + </configuration> + </plugin> + + </plugins> + </build> +</project> diff --git a/src/main/java/com/squareup/javapoet/AnnotationSpec.java b/src/main/java/com/squareup/javapoet/AnnotationSpec.java new file mode 100644 index 0000000..d1c5e53 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/AnnotationSpec.java @@ -0,0 +1,279 @@ +/* + * 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.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleAnnotationValueVisitor8; + +import static com.squareup.javapoet.Util.characterLiteralWithoutSingleQuotes; +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; + +/** A generated annotation on a declaration. */ +public final class AnnotationSpec { + public final TypeName type; + public final Map<String, List<CodeBlock>> members; + + private AnnotationSpec(Builder builder) { + this.type = builder.type; + this.members = Util.immutableMultimap(builder.members); + } + + void emit(CodeWriter codeWriter, boolean inline) throws IOException { + String whitespace = inline ? "" : "\n"; + String memberSeparator = inline ? ", " : ",\n"; + if (members.isEmpty()) { + // @Singleton + codeWriter.emit("@$T", type); + } else if (members.size() == 1 && members.containsKey("value")) { + // @Named("foo") + codeWriter.emit("@$T(", type); + emitAnnotationValues(codeWriter, whitespace, memberSeparator, members.get("value")); + codeWriter.emit(")"); + } else { + // Inline: + // @Column(name = "updated_at", nullable = false) + // + // Not inline: + // @Column( + // name = "updated_at", + // nullable = false + // ) + codeWriter.emit("@$T(" + whitespace, type); + codeWriter.indent(2); + for (Iterator<Map.Entry<String, List<CodeBlock>>> i + = members.entrySet().iterator(); i.hasNext(); ) { + Map.Entry<String, List<CodeBlock>> entry = i.next(); + codeWriter.emit("$L = ", entry.getKey()); + emitAnnotationValues(codeWriter, whitespace, memberSeparator, entry.getValue()); + if (i.hasNext()) codeWriter.emit(memberSeparator); + } + codeWriter.unindent(2); + codeWriter.emit(whitespace + ")"); + } + } + + private void emitAnnotationValues(CodeWriter codeWriter, String whitespace, + String memberSeparator, List<CodeBlock> values) throws IOException { + if (values.size() == 1) { + codeWriter.indent(2); + codeWriter.emit(values.get(0)); + codeWriter.unindent(2); + return; + } + + codeWriter.emit("{" + whitespace); + codeWriter.indent(2); + boolean first = true; + for (CodeBlock codeBlock : values) { + if (!first) codeWriter.emit(memberSeparator); + codeWriter.emit(codeBlock); + first = false; + } + codeWriter.unindent(2); + codeWriter.emit(whitespace + "}"); + } + + public static AnnotationSpec get(Annotation annotation) { + return get(annotation, false); + } + + public static AnnotationSpec get(Annotation annotation, boolean includeDefaultValues) { + Builder builder = builder(annotation.annotationType()); + try { + Method[] methods = annotation.annotationType().getDeclaredMethods(); + Arrays.sort(methods, Comparator.comparing(Method::getName)); + for (Method method : methods) { + Object value = method.invoke(annotation); + if (!includeDefaultValues) { + if (Objects.deepEquals(value, method.getDefaultValue())) { + continue; + } + } + if (value.getClass().isArray()) { + for (int i = 0; i < Array.getLength(value); i++) { + builder.addMemberForValue(method.getName(), Array.get(value, i)); + } + continue; + } + if (value instanceof Annotation) { + builder.addMember(method.getName(), "$L", get((Annotation) value)); + continue; + } + builder.addMemberForValue(method.getName(), value); + } + } catch (Exception e) { + throw new RuntimeException("Reflecting " + annotation + " failed!", e); + } + return builder.build(); + } + + public static AnnotationSpec get(AnnotationMirror annotation) { + TypeElement element = (TypeElement) annotation.getAnnotationType().asElement(); + AnnotationSpec.Builder builder = AnnotationSpec.builder(ClassName.get(element)); + Visitor visitor = new Visitor(builder); + for (ExecutableElement executableElement : annotation.getElementValues().keySet()) { + String name = executableElement.getSimpleName().toString(); + AnnotationValue value = annotation.getElementValues().get(executableElement); + value.accept(visitor, name); + } + return builder.build(); + } + + public static Builder builder(ClassName type) { + checkNotNull(type, "type == null"); + return new Builder(type); + } + + public static Builder builder(Class<?> type) { + return builder(ClassName.get(type)); + } + + public Builder toBuilder() { + Builder builder = new Builder(type); + for (Map.Entry<String, List<CodeBlock>> entry : members.entrySet()) { + builder.members.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return builder; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + StringBuilder out = new StringBuilder(); + try { + CodeWriter codeWriter = new CodeWriter(out); + codeWriter.emit("$L", this); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public static final class Builder { + private final TypeName type; + private final Map<String, List<CodeBlock>> members = new LinkedHashMap<>(); + + private Builder(TypeName type) { + this.type = type; + } + + public Builder addMember(String name, String format, Object... args) { + return addMember(name, CodeBlock.of(format, args)); + } + + public Builder addMember(String name, CodeBlock codeBlock) { + checkNotNull(name, "name == null"); + checkArgument(SourceVersion.isName(name), "not a valid name: %s", name); + List<CodeBlock> values = members.computeIfAbsent(name, k -> new ArrayList<>()); + values.add(codeBlock); + return this; + } + + /** + * Delegates to {@link #addMember(String, String, Object...)}, with parameter {@code format} + * depending on the given {@code value} object. Falls back to {@code "$L"} literal format if + * the class of the given {@code value} object is not supported. + */ + Builder addMemberForValue(String memberName, Object value) { + checkNotNull(memberName, "memberName == null"); + checkNotNull(value, "value == null, constant non-null value expected for %s", memberName); + checkArgument(SourceVersion.isName(memberName), "not a valid name: %s", memberName); + if (value instanceof Class<?>) { + return addMember(memberName, "$T.class", value); + } + if (value instanceof Enum) { + return addMember(memberName, "$T.$L", value.getClass(), ((Enum<?>) value).name()); + } + if (value instanceof String) { + return addMember(memberName, "$S", value); + } + if (value instanceof Float) { + return addMember(memberName, "$Lf", value); + } + if (value instanceof Character) { + return addMember(memberName, "'$L'", characterLiteralWithoutSingleQuotes((char) value)); + } + return addMember(memberName, "$L", value); + } + + public AnnotationSpec build() { + return new AnnotationSpec(this); + } + } + + /** + * Annotation value visitor adding members to the given builder instance. + */ + private static class Visitor extends SimpleAnnotationValueVisitor8<Builder, String> { + final Builder builder; + + Visitor(Builder builder) { + super(builder); + this.builder = builder; + } + + @Override protected Builder defaultAction(Object o, String name) { + return builder.addMemberForValue(name, o); + } + + @Override public Builder visitAnnotation(AnnotationMirror a, String name) { + return builder.addMember(name, "$L", get(a)); + } + + @Override public Builder visitEnumConstant(VariableElement c, String name) { + return builder.addMember(name, "$T.$L", c.asType(), c.getSimpleName()); + } + + @Override public Builder visitType(TypeMirror t, String name) { + return builder.addMember(name, "$T.class", t); + } + + @Override public Builder visitArray(List<? extends AnnotationValue> values, String name) { + for (AnnotationValue value : values) { + value.accept(this, name); + } + return builder; + } + } +} diff --git a/src/main/java/com/squareup/javapoet/ArrayTypeName.java b/src/main/java/com/squareup/javapoet/ArrayTypeName.java new file mode 100644 index 0000000..219c3f3 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/ArrayTypeName.java @@ -0,0 +1,109 @@ +/* + * 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.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.ArrayType; + +import static com.squareup.javapoet.Util.checkNotNull; + +public final class ArrayTypeName extends TypeName { + public final TypeName componentType; + + private ArrayTypeName(TypeName componentType) { + this(componentType, new ArrayList<>()); + } + + private ArrayTypeName(TypeName componentType, List<AnnotationSpec> annotations) { + super(annotations); + this.componentType = checkNotNull(componentType, "rawType == null"); + } + + @Override public ArrayTypeName annotated(List<AnnotationSpec> annotations) { + return new ArrayTypeName(componentType, concatAnnotations(annotations)); + } + + @Override public TypeName withoutAnnotations() { + return new ArrayTypeName(componentType); + } + + @Override CodeWriter emit(CodeWriter out) throws IOException { + return emit(out, false); + } + + CodeWriter emit(CodeWriter out, boolean varargs) throws IOException { + emitLeafType(out); + return emitBrackets(out, varargs); + } + + private CodeWriter emitLeafType(CodeWriter out) throws IOException { + if (TypeName.asArray(componentType) != null) { + return TypeName.asArray(componentType).emitLeafType(out); + } + return componentType.emit(out); + } + + private CodeWriter emitBrackets(CodeWriter out, boolean varargs) throws IOException { + if (isAnnotated()) { + out.emit(" "); + emitAnnotations(out); + } + + if (TypeName.asArray(componentType) == null) { + // Last bracket. + return out.emit(varargs ? "..." : "[]"); + } + out.emit("[]"); + return TypeName.asArray(componentType) .emitBrackets(out, varargs); + } + + + /** Returns an array type whose elements are all instances of {@code componentType}. */ + public static ArrayTypeName of(TypeName componentType) { + return new ArrayTypeName(componentType); + } + + /** Returns an array type whose elements are all instances of {@code componentType}. */ + public static ArrayTypeName of(Type componentType) { + return of(TypeName.get(componentType)); + } + + /** Returns an array type equivalent to {@code mirror}. */ + public static ArrayTypeName get(ArrayType mirror) { + return get(mirror, new LinkedHashMap<>()); + } + + static ArrayTypeName get( + ArrayType mirror, Map<TypeParameterElement, TypeVariableName> typeVariables) { + return new ArrayTypeName(get(mirror.getComponentType(), typeVariables)); + } + + /** Returns an array type equivalent to {@code type}. */ + public static ArrayTypeName get(GenericArrayType type) { + return get(type, new LinkedHashMap<>()); + } + + static ArrayTypeName get(GenericArrayType type, Map<Type, TypeVariableName> map) { + return ArrayTypeName.of(get(type.getGenericComponentType(), map)); + } +} diff --git a/src/main/java/com/squareup/javapoet/ClassName.java b/src/main/java/com/squareup/javapoet/ClassName.java new file mode 100644 index 0000000..99c4ed2 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/ClassName.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2014 Google, 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.List; +import java.util.Map; +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.SimpleElementVisitor8; + +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; + +/** A fully-qualified class name for top-level and member classes. */ +public final class ClassName extends TypeName implements Comparable<ClassName> { + public static final ClassName OBJECT = ClassName.get(Object.class); + + /** The package name of this class, or "" if this is in the default package. */ + final String packageName; + + /** The enclosing class, or null if this is not enclosed in another class. */ + final ClassName enclosingClassName; + + /** This class name, like "Entry" for java.util.Map.Entry. */ + final String simpleName; + + /** The full class name like "java.util.Map.Entry". */ + final String canonicalName; + + private ClassName(String packageName, ClassName enclosingClassName, String simpleName) { + this(packageName, enclosingClassName, simpleName, Collections.emptyList()); + } + + private ClassName(String packageName, ClassName enclosingClassName, String simpleName, + List<AnnotationSpec> annotations) { + super(annotations); + this.packageName = packageName; + this.enclosingClassName = enclosingClassName; + this.simpleName = simpleName; + this.canonicalName = enclosingClassName != null + ? (enclosingClassName.canonicalName + '.' + simpleName) + : (packageName.isEmpty() ? simpleName : packageName + '.' + simpleName); + } + + @Override public ClassName annotated(List<AnnotationSpec> annotations) { + return new ClassName(packageName, enclosingClassName, simpleName, + concatAnnotations(annotations)); + } + + @Override public ClassName withoutAnnotations() { + if (!isAnnotated()) return this; + ClassName resultEnclosingClassName = enclosingClassName != null + ? enclosingClassName.withoutAnnotations() + : null; + return new ClassName(packageName, resultEnclosingClassName, simpleName); + } + + @Override public boolean isAnnotated() { + return super.isAnnotated() || (enclosingClassName != null && enclosingClassName.isAnnotated()); + } + + /** + * Returns the package name, like {@code "java.util"} for {@code Map.Entry}. Returns the empty + * string for the default package. + */ + public String packageName() { + return packageName; + } + + /** + * Returns the enclosing class, like {@link Map} for {@code Map.Entry}. Returns null if this class + * is not nested in another class. + */ + public ClassName enclosingClassName() { + return enclosingClassName; + } + + /** + * Returns the top class in this nesting group. Equivalent to chained calls to {@link + * #enclosingClassName()} until the result's enclosing class is null. + */ + public ClassName topLevelClassName() { + return enclosingClassName != null ? enclosingClassName.topLevelClassName() : this; + } + + /** Return the binary name of a class. */ + public String reflectionName() { + return enclosingClassName != null + ? (enclosingClassName.reflectionName() + '$' + simpleName) + : (packageName.isEmpty() ? simpleName : packageName + '.' + simpleName); + } + + public List<String> simpleNames() { + List<String> simpleNames = new ArrayList<>(); + if (enclosingClassName != null) { + simpleNames.addAll(enclosingClassName().simpleNames()); + } + simpleNames.add(simpleName); + return simpleNames; + } + + /** + * Returns a class that shares the same enclosing package or class. If this class is enclosed by + * another class, this is equivalent to {@code enclosingClassName().nestedClass(name)}. Otherwise + * it is equivalent to {@code get(packageName(), name)}. + */ + public ClassName peerClass(String name) { + return new ClassName(packageName, enclosingClassName, name); + } + + /** + * Returns a new {@link ClassName} instance for the specified {@code name} as nested inside this + * class. + */ + public ClassName nestedClass(String name) { + return new ClassName(packageName, this, name); + } + + /** Returns the simple name of this class, like {@code "Entry"} for {@link Map.Entry}. */ + public String simpleName() { + return simpleName; + } + + public static ClassName get(Class<?> clazz) { + checkNotNull(clazz, "clazz == null"); + checkArgument(!clazz.isPrimitive(), "primitive types cannot be represented as a ClassName"); + checkArgument(!void.class.equals(clazz), "'void' type cannot be represented as a ClassName"); + checkArgument(!clazz.isArray(), "array types cannot be represented as a ClassName"); + + String anonymousSuffix = ""; + while (clazz.isAnonymousClass()) { + int lastDollar = clazz.getName().lastIndexOf('$'); + anonymousSuffix = clazz.getName().substring(lastDollar) + anonymousSuffix; + clazz = clazz.getEnclosingClass(); + } + String name = clazz.getSimpleName() + anonymousSuffix; + + if (clazz.getEnclosingClass() == null) { + // Avoid unreliable Class.getPackage(). https://github.com/square/javapoet/issues/295 + int lastDot = clazz.getName().lastIndexOf('.'); + String packageName = (lastDot != -1) ? clazz.getName().substring(0, lastDot) : null; + return new ClassName(packageName, null, name); + } + + return ClassName.get(clazz.getEnclosingClass()).nestedClass(name); + } + + /** + * Returns a new {@link ClassName} instance for the given fully-qualified class name string. This + * method assumes that the input is ASCII and follows typical Java style (lowercase package + * names, UpperCamelCase class names) and may produce incorrect results or throw + * {@link IllegalArgumentException} otherwise. For that reason, {@link #get(Class)} and + * {@link #get(Class)} should be preferred as they can correctly create {@link ClassName} + * instances without such restrictions. + */ + public static ClassName bestGuess(String classNameString) { + // Add the package name, like "java.util.concurrent", or "" for no package. + int p = 0; + while (p < classNameString.length() && Character.isLowerCase(classNameString.codePointAt(p))) { + p = classNameString.indexOf('.', p) + 1; + checkArgument(p != 0, "couldn't make a guess for %s", classNameString); + } + String packageName = p == 0 ? "" : classNameString.substring(0, p - 1); + + // Add class names like "Map" and "Entry". + ClassName className = null; + for (String simpleName : classNameString.substring(p).split("\\.", -1)) { + checkArgument(!simpleName.isEmpty() && Character.isUpperCase(simpleName.codePointAt(0)), + "couldn't make a guess for %s", classNameString); + className = new ClassName(packageName, className, simpleName); + } + + return className; + } + + /** + * Returns a class name created from the given parts. For example, calling this with package name + * {@code "java.util"} and simple names {@code "Map"}, {@code "Entry"} yields {@link Map.Entry}. + */ + public static ClassName get(String packageName, String simpleName, String... simpleNames) { + ClassName className = new ClassName(packageName, null, simpleName); + for (String name : simpleNames) { + className = className.nestedClass(name); + } + return className; + } + + /** Returns the class name for {@code element}. */ + public static ClassName get(TypeElement element) { + checkNotNull(element, "element == null"); + String simpleName = element.getSimpleName().toString(); + + return element.getEnclosingElement().accept(new SimpleElementVisitor8<ClassName, Void>() { + @Override public ClassName visitPackage(PackageElement packageElement, Void p) { + return new ClassName(packageElement.getQualifiedName().toString(), null, simpleName); + } + + @Override public ClassName visitType(TypeElement enclosingClass, Void p) { + return ClassName.get(enclosingClass).nestedClass(simpleName); + } + + @Override public ClassName visitUnknown(Element unknown, Void p) { + return get("", simpleName); + } + + @Override public ClassName defaultAction(Element enclosingElement, Void p) { + throw new IllegalArgumentException("Unexpected type nesting: " + element); + } + }, null); + } + + @Override public int compareTo(ClassName o) { + return canonicalName.compareTo(o.canonicalName); + } + + @Override CodeWriter emit(CodeWriter out) throws IOException { + boolean charsEmitted = false; + for (ClassName className : enclosingClasses()) { + String simpleName; + if (charsEmitted) { + // We've already emitted an enclosing class. Emit as we go. + out.emit("."); + simpleName = className.simpleName; + + } else if (className.isAnnotated() || className == this) { + // We encountered the first enclosing class that must be emitted. + String qualifiedName = out.lookupName(className); + int dot = qualifiedName.lastIndexOf('.'); + if (dot != -1) { + out.emitAndIndent(qualifiedName.substring(0, dot + 1)); + simpleName = qualifiedName.substring(dot + 1); + charsEmitted = true; + } else { + simpleName = qualifiedName; + } + + } else { + // Don't emit this enclosing type. Keep going so we can be more precise. + continue; + } + + if (className.isAnnotated()) { + if (charsEmitted) out.emit(" "); + className.emitAnnotations(out); + } + + out.emit(simpleName); + charsEmitted = true; + } + + return out; + } + + /** Returns all enclosing classes in this, outermost first. */ + private List<ClassName> enclosingClasses() { + List<ClassName> result = new ArrayList<>(); + for (ClassName c = this; c != null; c = c.enclosingClassName) { + result.add(c); + } + Collections.reverse(result); + return result; + } +} diff --git a/src/main/java/com/squareup/javapoet/CodeBlock.java b/src/main/java/com/squareup/javapoet/CodeBlock.java new file mode 100644 index 0000000..33e3846 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/CodeBlock.java @@ -0,0 +1,464 @@ +/* + * 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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collector; +import java.util.stream.StreamSupport; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; + +import static com.squareup.javapoet.Util.checkArgument; + +/** + * A fragment of a .java file, potentially containing declarations, statements, and documentation. + * Code blocks are not necessarily well-formed Java code, and are not validated. This class assumes + * javac will check correctness later! + * + * <p>Code blocks support placeholders like {@link java.text.Format}. Where {@link String#format} + * uses percent {@code %} to reference target values, this class uses dollar sign {@code $} and has + * its own set of permitted placeholders: + * + * <ul> + * <li>{@code $L} emits a <em>literal</em> value with no escaping. Arguments for literals may be + * strings, primitives, {@linkplain TypeSpec type declarations}, {@linkplain AnnotationSpec + * annotations} and even other code blocks. + * <li>{@code $N} emits a <em>name</em>, using name collision avoidance where necessary. Arguments + * for names may be strings (actually any {@linkplain CharSequence character sequence}), + * {@linkplain ParameterSpec parameters}, {@linkplain FieldSpec fields}, {@linkplain + * MethodSpec methods}, and {@linkplain TypeSpec types}. + * <li>{@code $S} escapes the value as a <em>string</em>, wraps it with double quotes, and emits + * that. For example, {@code 6" sandwich} is emitted {@code "6\" sandwich"}. + * <li>{@code $T} emits a <em>type</em> reference. Types will be imported if possible. Arguments + * for types may be {@linkplain Class classes}, {@linkplain javax.lang.model.type.TypeMirror +,* type mirrors}, and {@linkplain javax.lang.model.element.Element elements}. + * <li>{@code $$} emits a dollar sign. + * <li>{@code $W} emits a space or a newline, depending on its position on the line. This prefers + * to wrap lines before 100 columns. + * <li>{@code $Z} acts as a zero-width space. This prefers to wrap lines before 100 columns. + * <li>{@code $>} increases the indentation level. + * <li>{@code $<} decreases the indentation level. + * <li>{@code $[} begins a statement. For multiline statements, every line after the first line + * is double-indented. + * <li>{@code $]} ends a statement. + * </ul> + */ +public final class CodeBlock { + private static final Pattern NAMED_ARGUMENT = + Pattern.compile("\\$(?<argumentName>[\\w_]+):(?<typeChar>[\\w]).*"); + private static final Pattern LOWERCASE = Pattern.compile("[a-z]+[\\w_]*"); + + /** A heterogeneous list containing string literals and value placeholders. */ + final List<String> formatParts; + final List<Object> args; + + private CodeBlock(Builder builder) { + this.formatParts = Util.immutableList(builder.formatParts); + this.args = Util.immutableList(builder.args); + } + + public boolean isEmpty() { + return formatParts.isEmpty(); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + StringBuilder out = new StringBuilder(); + try { + new CodeWriter(out).emit(this); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public static CodeBlock of(String format, Object... args) { + return new Builder().add(format, args).build(); + } + + /** + * Joins {@code codeBlocks} into a single {@link CodeBlock}, each separated by {@code separator}. + * For example, joining {@code String s}, {@code Object o} and {@code int i} using {@code ", "} + * would produce {@code String s, Object o, int i}. + */ + public static CodeBlock join(Iterable<CodeBlock> codeBlocks, String separator) { + return StreamSupport.stream(codeBlocks.spliterator(), false).collect(joining(separator)); + } + + /** + * A {@link Collector} implementation that joins {@link CodeBlock} instances together into one + * separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and + * {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}. + */ + public static Collector<CodeBlock, ?, CodeBlock> joining(String separator) { + return Collector.of( + () -> new CodeBlockJoiner(separator, builder()), + CodeBlockJoiner::add, + CodeBlockJoiner::merge, + CodeBlockJoiner::join); + } + + /** + * A {@link Collector} implementation that joins {@link CodeBlock} instances together into one + * separated by {@code separator}. For example, joining {@code String s}, {@code Object o} and + * {@code int i} using {@code ", "} would produce {@code String s, Object o, int i}. + */ + public static Collector<CodeBlock, ?, CodeBlock> joining( + String separator, String prefix, String suffix) { + Builder builder = builder().add("$N", prefix); + return Collector.of( + () -> new CodeBlockJoiner(separator, builder), + CodeBlockJoiner::add, + CodeBlockJoiner::merge, + joiner -> { + builder.add(CodeBlock.of("$N", suffix)); + return joiner.join(); + }); + } + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + Builder builder = new Builder(); + builder.formatParts.addAll(formatParts); + builder.args.addAll(args); + return builder; + } + + public static final class Builder { + final List<String> formatParts = new ArrayList<>(); + final List<Object> args = new ArrayList<>(); + + private Builder() { + } + + public boolean isEmpty() { + return formatParts.isEmpty(); + } + + /** + * Adds code using named arguments. + * + * <p>Named arguments specify their name after the '$' followed by : and the corresponding type + * character. Argument names consist of characters in {@code a-z, A-Z, 0-9, and _} and must + * start with a lowercase character. + * + * <p>For example, to refer to the type {@link java.lang.Integer} with the argument name {@code + * clazz} use a format string containing {@code $clazz:T} and include the key {@code clazz} with + * value {@code java.lang.Integer.class} in the argument map. + */ + public Builder addNamed(String format, Map<String, ?> arguments) { + int p = 0; + + for (String argument : arguments.keySet()) { + checkArgument(LOWERCASE.matcher(argument).matches(), + "argument '%s' must start with a lowercase character", argument); + } + + while (p < format.length()) { + int nextP = format.indexOf("$", p); + if (nextP == -1) { + formatParts.add(format.substring(p, format.length())); + break; + } + + if (p != nextP) { + formatParts.add(format.substring(p, nextP)); + p = nextP; + } + + Matcher matcher = null; + int colon = format.indexOf(':', p); + if (colon != -1) { + int endIndex = Math.min(colon + 2, format.length()); + matcher = NAMED_ARGUMENT.matcher(format.substring(p, endIndex)); + } + if (matcher != null && matcher.lookingAt()) { + String argumentName = matcher.group("argumentName"); + checkArgument(arguments.containsKey(argumentName), "Missing named argument for $%s", + argumentName); + char formatChar = matcher.group("typeChar").charAt(0); + addArgument(format, formatChar, arguments.get(argumentName)); + formatParts.add("$" + formatChar); + p += matcher.regionEnd(); + } else { + checkArgument(p < format.length() - 1, "dangling $ at end"); + checkArgument(isNoArgPlaceholder(format.charAt(p + 1)), + "unknown format $%s at %s in '%s'", format.charAt(p + 1), p + 1, format); + formatParts.add(format.substring(p, p + 2)); + p += 2; + } + } + + return this; + } + + /** + * Add code with positional or relative arguments. + * + * <p>Relative arguments map 1:1 with the placeholders in the format string. + * + * <p>Positional arguments use an index after the placeholder to identify which argument index + * to use. For example, for a literal to reference the 3rd argument: "$3L" (1 based index) + * + * <p>Mixing relative and positional arguments in a call to add is invalid and will result in an + * error. + */ + public Builder add(String format, Object... args) { + boolean hasRelative = false; + boolean hasIndexed = false; + + int relativeParameterCount = 0; + int[] indexedParameterCount = new int[args.length]; + + for (int p = 0; p < format.length(); ) { + if (format.charAt(p) != '$') { + int nextP = format.indexOf('$', p + 1); + if (nextP == -1) nextP = format.length(); + formatParts.add(format.substring(p, nextP)); + p = nextP; + continue; + } + + p++; // '$'. + + // Consume zero or more digits, leaving 'c' as the first non-digit char after the '$'. + int indexStart = p; + char c; + do { + checkArgument(p < format.length(), "dangling format characters in '%s'", format); + c = format.charAt(p++); + } while (c >= '0' && c <= '9'); + int indexEnd = p - 1; + + // If 'c' doesn't take an argument, we're done. + if (isNoArgPlaceholder(c)) { + checkArgument( + indexStart == indexEnd, "$$, $>, $<, $[, $], $W, and $Z may not have an index"); + formatParts.add("$" + c); + continue; + } + + // Find either the indexed argument, or the relative argument. (0-based). + int index; + if (indexStart < indexEnd) { + index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1; + hasIndexed = true; + if (args.length > 0) { + indexedParameterCount[index % args.length]++; // modulo is needed, checked below anyway + } + } else { + index = relativeParameterCount; + hasRelative = true; + relativeParameterCount++; + } + + checkArgument(index >= 0 && index < args.length, + "index %d for '%s' not in range (received %s arguments)", + index + 1, format.substring(indexStart - 1, indexEnd + 1), args.length); + checkArgument(!hasIndexed || !hasRelative, "cannot mix indexed and positional parameters"); + + addArgument(format, c, args[index]); + + formatParts.add("$" + c); + } + + if (hasRelative) { + checkArgument(relativeParameterCount >= args.length, + "unused arguments: expected %s, received %s", relativeParameterCount, args.length); + } + if (hasIndexed) { + List<String> unused = new ArrayList<>(); + for (int i = 0; i < args.length; i++) { + if (indexedParameterCount[i] == 0) { + unused.add("$" + (i + 1)); + } + } + String s = unused.size() == 1 ? "" : "s"; + checkArgument(unused.isEmpty(), "unused argument%s: %s", s, String.join(", ", unused)); + } + return this; + } + + private boolean isNoArgPlaceholder(char c) { + return c == '$' || c == '>' || c == '<' || c == '[' || c == ']' || c == 'W' || c == 'Z'; + } + + private void addArgument(String format, char c, Object arg) { + switch (c) { + case 'N': + this.args.add(argToName(arg)); + break; + case 'L': + this.args.add(argToLiteral(arg)); + break; + case 'S': + this.args.add(argToString(arg)); + break; + case 'T': + this.args.add(argToType(arg)); + break; + default: + throw new IllegalArgumentException( + String.format("invalid format string: '%s'", format)); + } + } + + private String argToName(Object o) { + if (o instanceof CharSequence) return o.toString(); + if (o instanceof ParameterSpec) return ((ParameterSpec) o).name; + if (o instanceof FieldSpec) return ((FieldSpec) o).name; + if (o instanceof MethodSpec) return ((MethodSpec) o).name; + if (o instanceof TypeSpec) return ((TypeSpec) o).name; + throw new IllegalArgumentException("expected name but was " + o); + } + + private Object argToLiteral(Object o) { + return o; + } + + private String argToString(Object o) { + return o != null ? String.valueOf(o) : null; + } + + private TypeName argToType(Object o) { + if (o instanceof TypeName) return (TypeName) o; + if (o instanceof TypeMirror) return TypeName.get((TypeMirror) o); + if (o instanceof Element) return TypeName.get(((Element) o).asType()); + if (o instanceof Type) return TypeName.get((Type) o); + throw new IllegalArgumentException("expected type but was " + o); + } + + /** + * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". + * Shouldn't contain braces or newline characters. + */ + public Builder beginControlFlow(String controlFlow, Object... args) { + add(controlFlow + " {\n", args); + indent(); + return this; + } + + /** + * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". + * Shouldn't contain braces or newline characters. + */ + public Builder nextControlFlow(String controlFlow, Object... args) { + unindent(); + add("} " + controlFlow + " {\n", args); + indent(); + return this; + } + + public Builder endControlFlow() { + unindent(); + add("}\n"); + return this; + } + + /** + * @param controlFlow the optional control flow construct and its code, such as + * "while(foo == 20)". Only used for "do/while" control flows. + */ + public Builder endControlFlow(String controlFlow, Object... args) { + unindent(); + add("} " + controlFlow + ";\n", args); + return this; + } + + public Builder addStatement(String format, Object... args) { + add("$["); + add(format, args); + add(";\n$]"); + return this; + } + + public Builder addStatement(CodeBlock codeBlock) { + return addStatement("$L", codeBlock); + } + + public Builder add(CodeBlock codeBlock) { + formatParts.addAll(codeBlock.formatParts); + args.addAll(codeBlock.args); + return this; + } + + public Builder indent() { + this.formatParts.add("$>"); + return this; + } + + public Builder unindent() { + this.formatParts.add("$<"); + return this; + } + + public CodeBlock build() { + return new CodeBlock(this); + } + } + + private static final class CodeBlockJoiner { + private final String delimiter; + private final Builder builder; + private boolean first = true; + + CodeBlockJoiner(String delimiter, Builder builder) { + this.delimiter = delimiter; + this.builder = builder; + } + + CodeBlockJoiner add(CodeBlock codeBlock) { + if (!first) { + builder.add(delimiter); + } + first = false; + + builder.add(codeBlock); + return this; + } + + CodeBlockJoiner merge(CodeBlockJoiner other) { + CodeBlock otherBlock = other.builder.build(); + if (!otherBlock.isEmpty()) { + add(otherBlock); + } + return this; + } + + CodeBlock join() { + return builder.build(); + } + } +} diff --git a/src/main/java/com/squareup/javapoet/CodeWriter.java b/src/main/java/com/squareup/javapoet/CodeWriter.java new file mode 100644 index 0000000..542f434 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/CodeWriter.java @@ -0,0 +1,497 @@ +/* + * 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.stringLiteralWithDoubleQuotes; +import static java.lang.String.join; + +/** + * 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.emptySet()); + } + + CodeWriter(Appendable out, String indent, Set<String> staticImports) { + this(out, indent, Collections.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 not set"); + 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.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(", "); + emitAnnotations(typeVariable.annotations, true); + 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++); + // 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; + + case "$Z": + out.zeroWidthSpace(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.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 (resolved != null && Objects.equals(resolved.canonicalName, c.canonicalName)) { + 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; + } +} diff --git a/src/main/java/com/squareup/javapoet/FieldSpec.java b/src/main/java/com/squareup/javapoet/FieldSpec.java new file mode 100644 index 0000000..851b36d --- /dev/null +++ b/src/main/java/com/squareup/javapoet/FieldSpec.java @@ -0,0 +1,174 @@ +/* + * 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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +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; + +/** A generated field declaration. */ +public final class FieldSpec { + public final TypeName type; + public final String name; + public final CodeBlock javadoc; + public final List<AnnotationSpec> annotations; + public final Set<Modifier> modifiers; + public final CodeBlock initializer; + + private FieldSpec(Builder builder) { + this.type = checkNotNull(builder.type, "type == null"); + this.name = checkNotNull(builder.name, "name == null"); + this.javadoc = builder.javadoc.build(); + this.annotations = Util.immutableList(builder.annotations); + this.modifiers = Util.immutableSet(builder.modifiers); + this.initializer = (builder.initializer == null) + ? CodeBlock.builder().build() + : builder.initializer; + } + + public boolean hasModifier(Modifier modifier) { + return modifiers.contains(modifier); + } + + void emit(CodeWriter codeWriter, Set<Modifier> implicitModifiers) throws IOException { + codeWriter.emitJavadoc(javadoc); + codeWriter.emitAnnotations(annotations, false); + codeWriter.emitModifiers(modifiers, implicitModifiers); + codeWriter.emit("$T $L", type, name); + if (!initializer.isEmpty()) { + codeWriter.emit(" = "); + codeWriter.emit(initializer); + } + codeWriter.emit(";\n"); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + StringBuilder out = new StringBuilder(); + try { + CodeWriter codeWriter = new CodeWriter(out); + emit(codeWriter, Collections.emptySet()); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public static Builder builder(TypeName type, String name, Modifier... modifiers) { + checkNotNull(type, "type == null"); + checkArgument(SourceVersion.isName(name), "not a valid name: %s", name); + return new Builder(type, name) + .addModifiers(modifiers); + } + + public static Builder builder(Type type, String name, Modifier... modifiers) { + return builder(TypeName.get(type), name, modifiers); + } + + public Builder toBuilder() { + Builder builder = new Builder(type, name); + builder.javadoc.add(javadoc); + builder.annotations.addAll(annotations); + builder.modifiers.addAll(modifiers); + builder.initializer = initializer.isEmpty() ? null : initializer; + return builder; + } + + public static final class Builder { + private final TypeName type; + private final String name; + + private final CodeBlock.Builder javadoc = CodeBlock.builder(); + private final List<AnnotationSpec> annotations = new ArrayList<>(); + private final List<Modifier> modifiers = new ArrayList<>(); + private CodeBlock initializer = null; + + private Builder(TypeName type, String name) { + this.type = type; + this.name = name; + } + + public Builder addJavadoc(String format, Object... args) { + javadoc.add(format, args); + return this; + } + + public Builder addJavadoc(CodeBlock block) { + javadoc.add(block); + return this; + } + + public Builder addAnnotations(Iterable<AnnotationSpec> annotationSpecs) { + checkArgument(annotationSpecs != null, "annotationSpecs == null"); + for (AnnotationSpec annotationSpec : annotationSpecs) { + this.annotations.add(annotationSpec); + } + return this; + } + + public Builder addAnnotation(AnnotationSpec annotationSpec) { + this.annotations.add(annotationSpec); + return this; + } + + public Builder addAnnotation(ClassName annotation) { + this.annotations.add(AnnotationSpec.builder(annotation).build()); + return this; + } + + public Builder addAnnotation(Class<?> annotation) { + return addAnnotation(ClassName.get(annotation)); + } + + public Builder addModifiers(Modifier... modifiers) { + Collections.addAll(this.modifiers, modifiers); + return this; + } + + public Builder initializer(String format, Object... args) { + return initializer(CodeBlock.of(format, args)); + } + + public Builder initializer(CodeBlock codeBlock) { + checkState(this.initializer == null, "initializer was already set"); + this.initializer = checkNotNull(codeBlock, "codeBlock == null"); + return this; + } + + public FieldSpec build() { + return new FieldSpec(this); + } + } +} diff --git a/src/main/java/com/squareup/javapoet/JavaFile.java b/src/main/java/com/squareup/javapoet/JavaFile.java new file mode 100644 index 0000000..e7662dd --- /dev/null +++ b/src/main/java/com/squareup/javapoet/JavaFile.java @@ -0,0 +1,274 @@ +/* + * 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.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.processing.Filer; +import javax.lang.model.element.Element; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.SimpleJavaFileObject; + +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** A Java file containing a single top level class. */ +public final class JavaFile { + private static final Appendable NULL_APPENDABLE = new Appendable() { + @Override public Appendable append(CharSequence charSequence) { + return this; + } + @Override public Appendable append(CharSequence charSequence, int start, int end) { + return this; + } + @Override public Appendable append(char c) { + return this; + } + }; + + public final CodeBlock fileComment; + public final String packageName; + public final TypeSpec typeSpec; + public final boolean skipJavaLangImports; + private final Set<String> staticImports; + private final String indent; + + private JavaFile(Builder builder) { + this.fileComment = builder.fileComment.build(); + this.packageName = builder.packageName; + this.typeSpec = builder.typeSpec; + this.skipJavaLangImports = builder.skipJavaLangImports; + this.staticImports = Util.immutableSet(builder.staticImports); + this.indent = builder.indent; + } + + public void writeTo(Appendable out) throws IOException { + // First pass: emit the entire class, just to collect the types we'll need to import. + CodeWriter importsCollector = new CodeWriter(NULL_APPENDABLE, indent, staticImports); + emit(importsCollector); + Map<String, ClassName> suggestedImports = importsCollector.suggestedImports(); + + // Second pass: write the code, taking advantage of the imports. + CodeWriter codeWriter = new CodeWriter(out, indent, suggestedImports, staticImports); + emit(codeWriter); + } + + /** Writes this to {@code directory} as UTF-8 using the standard directory structure. */ + public void writeTo(Path directory) throws IOException { + checkArgument(Files.notExists(directory) || Files.isDirectory(directory), + "path %s exists but is not a directory.", directory); + Path outputDirectory = directory; + if (!packageName.isEmpty()) { + for (String packageComponent : packageName.split("\\.")) { + outputDirectory = outputDirectory.resolve(packageComponent); + } + Files.createDirectories(outputDirectory); + } + + Path outputPath = outputDirectory.resolve(typeSpec.name + ".java"); + try (Writer writer = new OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8)) { + writeTo(writer); + } + } + + /** Writes this to {@code directory} as UTF-8 using the standard directory structure. */ + public void writeTo(File directory) throws IOException { + writeTo(directory.toPath()); + } + + /** Writes this to {@code filer}. */ + public void writeTo(Filer filer) throws IOException { + String fileName = packageName.isEmpty() + ? typeSpec.name + : packageName + "." + typeSpec.name; + List<Element> originatingElements = typeSpec.originatingElements; + JavaFileObject filerSourceFile = filer.createSourceFile(fileName, + originatingElements.toArray(new Element[originatingElements.size()])); + try (Writer writer = filerSourceFile.openWriter()) { + writeTo(writer); + } catch (Exception e) { + try { + filerSourceFile.delete(); + } catch (Exception ignored) { + } + throw e; + } + } + + private void emit(CodeWriter codeWriter) throws IOException { + codeWriter.pushPackage(packageName); + + if (!fileComment.isEmpty()) { + codeWriter.emitComment(fileComment); + } + + if (!packageName.isEmpty()) { + codeWriter.emit("package $L;\n", packageName); + codeWriter.emit("\n"); + } + + if (!staticImports.isEmpty()) { + for (String signature : staticImports) { + codeWriter.emit("import static $L;\n", signature); + } + codeWriter.emit("\n"); + } + + int importedTypesCount = 0; + for (ClassName className : new TreeSet<>(codeWriter.importedTypes().values())) { + if (skipJavaLangImports && className.packageName().equals("java.lang")) continue; + codeWriter.emit("import $L;\n", className.withoutAnnotations()); + importedTypesCount++; + } + + if (importedTypesCount > 0) { + codeWriter.emit("\n"); + } + + typeSpec.emit(codeWriter, null, Collections.emptySet()); + + codeWriter.popPackage(); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + try { + StringBuilder result = new StringBuilder(); + writeTo(result); + return result.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public JavaFileObject toJavaFileObject() { + URI uri = URI.create((packageName.isEmpty() + ? typeSpec.name + : packageName.replace('.', '/') + '/' + typeSpec.name) + + Kind.SOURCE.extension); + return new SimpleJavaFileObject(uri, Kind.SOURCE) { + private final long lastModified = System.currentTimeMillis(); + @Override public String getCharContent(boolean ignoreEncodingErrors) { + return JavaFile.this.toString(); + } + @Override public InputStream openInputStream() throws IOException { + return new ByteArrayInputStream(getCharContent(true).getBytes(UTF_8)); + } + @Override public long getLastModified() { + return lastModified; + } + }; + } + + public static Builder builder(String packageName, TypeSpec typeSpec) { + checkNotNull(packageName, "packageName == null"); + checkNotNull(typeSpec, "typeSpec == null"); + return new Builder(packageName, typeSpec); + } + + public Builder toBuilder() { + Builder builder = new Builder(packageName, typeSpec); + builder.fileComment.add(fileComment); + builder.skipJavaLangImports = skipJavaLangImports; + builder.indent = indent; + return builder; + } + + public static final class Builder { + private final String packageName; + private final TypeSpec typeSpec; + private final CodeBlock.Builder fileComment = CodeBlock.builder(); + private final Set<String> staticImports = new TreeSet<>(); + private boolean skipJavaLangImports; + private String indent = " "; + + private Builder(String packageName, TypeSpec typeSpec) { + this.packageName = packageName; + this.typeSpec = typeSpec; + } + + public Builder addFileComment(String format, Object... args) { + this.fileComment.add(format, args); + return this; + } + + public Builder addStaticImport(Enum<?> constant) { + return addStaticImport(ClassName.get(constant.getDeclaringClass()), constant.name()); + } + + public Builder addStaticImport(Class<?> clazz, String... names) { + return addStaticImport(ClassName.get(clazz), names); + } + + public Builder addStaticImport(ClassName className, String... names) { + checkArgument(className != null, "className == null"); + checkArgument(names != null, "names == null"); + checkArgument(names.length > 0, "names array is empty"); + for (String name : names) { + checkArgument(name != null, "null entry in names array: %s", Arrays.toString(names)); + staticImports.add(className.canonicalName + "." + name); + } + return this; + } + + /** + * Call this to omit imports for classes in {@code java.lang}, such as {@code java.lang.String}. + * + * <p>By default, JavaPoet explicitly imports types in {@code java.lang} to defend against + * naming conflicts. Suppose an (ill-advised) class is named {@code com.example.String}. When + * {@code java.lang} imports are skipped, generated code in {@code com.example} that references + * {@code java.lang.String} will get {@code com.example.String} instead. + */ + public Builder skipJavaLangImports(boolean skipJavaLangImports) { + this.skipJavaLangImports = skipJavaLangImports; + return this; + } + + public Builder indent(String indent) { + this.indent = indent; + return this; + } + + public JavaFile build() { + return new JavaFile(this); + } + } +} diff --git a/src/main/java/com/squareup/javapoet/LineWrapper.java b/src/main/java/com/squareup/javapoet/LineWrapper.java new file mode 100644 index 0000000..6aa3131 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/LineWrapper.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2016 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 static com.squareup.javapoet.Util.checkNotNull; + +/** + * Implements soft line wrapping on an appendable. To use, append characters using {@link #append} + * or soft-wrapping spaces using {@link #wrappingSpace}. + */ +final class LineWrapper { + private final Appendable out; + private final String indent; + private final int columnLimit; + private boolean closed; + + /** Characters written since the last wrapping space that haven't yet been flushed. */ + private final StringBuilder buffer = new StringBuilder(); + + /** The number of characters since the most recent newline. Includes both out and the buffer. */ + private int column = 0; + + /** + * -1 if we have no buffering; otherwise the number of {@code indent}s to write after wrapping. + */ + private int indentLevel = -1; + + /** + * Null if we have no buffering; otherwise the type to pass to the next call to {@link #flush}. + */ + private FlushType nextFlush; + + LineWrapper(Appendable out, String indent, int columnLimit) { + checkNotNull(out, "out == null"); + this.out = out; + this.indent = indent; + this.columnLimit = columnLimit; + } + + /** Emit {@code s}. This may be buffered to permit line wraps to be inserted. */ + void append(String s) throws IOException { + if (closed) throw new IllegalStateException("closed"); + + if (nextFlush != null) { + int nextNewline = s.indexOf('\n'); + + // If s doesn't cause the current line to cross the limit, buffer it and return. We'll decide + // whether or not we have to wrap it later. + if (nextNewline == -1 && column + s.length() <= columnLimit) { + buffer.append(s); + column += s.length(); + return; + } + + // Wrap if appending s would overflow the current line. + boolean wrap = nextNewline == -1 || column + nextNewline > columnLimit; + flush(wrap ? FlushType.WRAP : nextFlush); + } + + out.append(s); + int lastNewline = s.lastIndexOf('\n'); + column = lastNewline != -1 + ? s.length() - lastNewline - 1 + : column + s.length(); + } + + /** Emit either a space or a newline character. */ + void wrappingSpace(int indentLevel) throws IOException { + if (closed) throw new IllegalStateException("closed"); + + if (this.nextFlush != null) flush(nextFlush); + column++; // Increment the column even though the space is deferred to next call to flush(). + this.nextFlush = FlushType.SPACE; + this.indentLevel = indentLevel; + } + + /** Emit a newline character if the line will exceed it's limit, otherwise do nothing. */ + void zeroWidthSpace(int indentLevel) throws IOException { + if (closed) throw new IllegalStateException("closed"); + + if (column == 0) return; + if (this.nextFlush != null) flush(nextFlush); + this.nextFlush = FlushType.EMPTY; + this.indentLevel = indentLevel; + } + + /** Flush any outstanding text and forbid future writes to this line wrapper. */ + void close() throws IOException { + if (nextFlush != null) flush(nextFlush); + closed = true; + } + + /** Write the space followed by any buffered text that follows it. */ + private void flush(FlushType flushType) throws IOException { + switch (flushType) { + case WRAP: + out.append('\n'); + for (int i = 0; i < indentLevel; i++) { + out.append(indent); + } + column = indentLevel * indent.length(); + column += buffer.length(); + break; + case SPACE: + out.append(' '); + break; + case EMPTY: + break; + default: + throw new IllegalArgumentException("Unknown FlushType: " + flushType); + } + + out.append(buffer); + buffer.delete(0, buffer.length()); + indentLevel = -1; + nextFlush = null; + } + + private enum FlushType { + WRAP, SPACE, EMPTY; + } +} diff --git a/src/main/java/com/squareup/javapoet/MethodSpec.java b/src/main/java/com/squareup/javapoet/MethodSpec.java new file mode 100644 index 0000000..a2c7c43 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/MethodSpec.java @@ -0,0 +1,493 @@ +/* + * 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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.util.Types; + +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; +import static com.squareup.javapoet.Util.checkState; + +/** A generated constructor or method declaration. */ +public final class MethodSpec { + static final String CONSTRUCTOR = "<init>"; + + public final String name; + public final CodeBlock javadoc; + public final List<AnnotationSpec> annotations; + public final Set<Modifier> modifiers; + public final List<TypeVariableName> typeVariables; + public final TypeName returnType; + public final List<ParameterSpec> parameters; + public final boolean varargs; + public final List<TypeName> exceptions; + public final CodeBlock code; + public final CodeBlock defaultValue; + + private MethodSpec(Builder builder) { + CodeBlock code = builder.code.build(); + checkArgument(code.isEmpty() || !builder.modifiers.contains(Modifier.ABSTRACT), + "abstract method %s cannot have code", builder.name); + checkArgument(!builder.varargs || lastParameterIsArray(builder.parameters), + "last parameter of varargs method %s must be an array", builder.name); + + this.name = checkNotNull(builder.name, "name == null"); + this.javadoc = builder.javadoc.build(); + this.annotations = Util.immutableList(builder.annotations); + this.modifiers = Util.immutableSet(builder.modifiers); + this.typeVariables = Util.immutableList(builder.typeVariables); + this.returnType = builder.returnType; + this.parameters = Util.immutableList(builder.parameters); + this.varargs = builder.varargs; + this.exceptions = Util.immutableList(builder.exceptions); + this.defaultValue = builder.defaultValue; + this.code = code; + } + + private boolean lastParameterIsArray(List<ParameterSpec> parameters) { + return !parameters.isEmpty() + && TypeName.asArray((parameters.get(parameters.size() - 1).type)) != null; + } + + void emit(CodeWriter codeWriter, String enclosingName, Set<Modifier> implicitModifiers) + throws IOException { + codeWriter.emitJavadoc(javadoc); + codeWriter.emitAnnotations(annotations, false); + codeWriter.emitModifiers(modifiers, implicitModifiers); + + if (!typeVariables.isEmpty()) { + codeWriter.emitTypeVariables(typeVariables); + codeWriter.emit(" "); + } + + if (isConstructor()) { + codeWriter.emit("$L($Z", enclosingName); + } else { + codeWriter.emit("$T $L($Z", returnType, name); + } + + boolean firstParameter = true; + for (Iterator<ParameterSpec> i = parameters.iterator(); i.hasNext(); ) { + ParameterSpec parameter = i.next(); + if (!firstParameter) codeWriter.emit(",").emitWrappingSpace(); + parameter.emit(codeWriter, !i.hasNext() && varargs); + firstParameter = false; + } + + codeWriter.emit(")"); + + if (defaultValue != null && !defaultValue.isEmpty()) { + codeWriter.emit(" default "); + codeWriter.emit(defaultValue); + } + + if (!exceptions.isEmpty()) { + codeWriter.emitWrappingSpace().emit("throws"); + boolean firstException = true; + for (TypeName exception : exceptions) { + if (!firstException) codeWriter.emit(","); + codeWriter.emitWrappingSpace().emit("$T", exception); + firstException = false; + } + } + + if (hasModifier(Modifier.ABSTRACT)) { + codeWriter.emit(";\n"); + } else if (hasModifier(Modifier.NATIVE)) { + // Code is allowed to support stuff like GWT JSNI. + codeWriter.emit(code); + codeWriter.emit(";\n"); + } else { + codeWriter.emit(" {\n"); + + codeWriter.indent(); + codeWriter.emit(code); + codeWriter.unindent(); + + codeWriter.emit("}\n"); + } + } + + public boolean hasModifier(Modifier modifier) { + return modifiers.contains(modifier); + } + + public boolean isConstructor() { + return name.equals(CONSTRUCTOR); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + StringBuilder out = new StringBuilder(); + try { + CodeWriter codeWriter = new CodeWriter(out); + emit(codeWriter, "Constructor", Collections.emptySet()); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public static Builder methodBuilder(String name) { + return new Builder(name); + } + + public static Builder constructorBuilder() { + return new Builder(CONSTRUCTOR); + } + + /** + * Returns a new method spec builder that overrides {@code method}. + * + * <p>This will copy its visibility modifiers, type parameters, return type, name, parameters, and + * throws declarations. An {@link Override} annotation will be added. + * + * <p>Note that in JavaPoet 1.2 through 1.7 this method retained annotations from the method and + * parameters of the overridden method. Since JavaPoet 1.8 annotations must be added separately. + */ + public static Builder overriding(ExecutableElement method) { + checkNotNull(method, "method == null"); + + Element enclosingClass = method.getEnclosingElement(); + if (enclosingClass.getModifiers().contains(Modifier.FINAL)) { + throw new IllegalArgumentException("Cannot override method on final class " + enclosingClass); + } + + Set<Modifier> modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.PRIVATE) + || modifiers.contains(Modifier.FINAL) + || modifiers.contains(Modifier.STATIC)) { + throw new IllegalArgumentException("cannot override method with modifiers: " + modifiers); + } + + String methodName = method.getSimpleName().toString(); + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodName); + + methodBuilder.addAnnotation(Override.class); + + modifiers = new LinkedHashSet<>(modifiers); + modifiers.remove(Modifier.ABSTRACT); + modifiers.remove(Modifier.DEFAULT); + methodBuilder.addModifiers(modifiers); + + for (TypeParameterElement typeParameterElement : method.getTypeParameters()) { + TypeVariable var = (TypeVariable) typeParameterElement.asType(); + methodBuilder.addTypeVariable(TypeVariableName.get(var)); + } + + methodBuilder.returns(TypeName.get(method.getReturnType())); + methodBuilder.addParameters(ParameterSpec.parametersOf(method)); + methodBuilder.varargs(method.isVarArgs()); + + for (TypeMirror thrownType : method.getThrownTypes()) { + methodBuilder.addException(TypeName.get(thrownType)); + } + + return methodBuilder; + } + + /** + * Returns a new method spec builder that overrides {@code method} as a member of {@code + * enclosing}. This will resolve type parameters: for example overriding {@link + * Comparable#compareTo} in a type that implements {@code Comparable<Movie>}, the {@code T} + * parameter will be resolved to {@code Movie}. + * + * <p>This will copy its visibility modifiers, type parameters, return type, name, parameters, and + * throws declarations. An {@link Override} annotation will be added. + * + * <p>Note that in JavaPoet 1.2 through 1.7 this method retained annotations from the method and + * parameters of the overridden method. Since JavaPoet 1.8 annotations must be added separately. + */ + public static Builder overriding( + ExecutableElement method, DeclaredType enclosing, Types types) { + ExecutableType executableType = (ExecutableType) types.asMemberOf(enclosing, method); + List<? extends TypeMirror> resolvedParameterTypes = executableType.getParameterTypes(); + List<? extends TypeMirror> resolvedThrownTypes = executableType.getThrownTypes(); + TypeMirror resolvedReturnType = executableType.getReturnType(); + + Builder builder = overriding(method); + builder.returns(TypeName.get(resolvedReturnType)); + for (int i = 0, size = builder.parameters.size(); i < size; i++) { + ParameterSpec parameter = builder.parameters.get(i); + TypeName type = TypeName.get(resolvedParameterTypes.get(i)); + builder.parameters.set(i, parameter.toBuilder(type, parameter.name).build()); + } + builder.exceptions.clear(); + for (int i = 0, size = resolvedThrownTypes.size(); i < size; i++) { + builder.addException(TypeName.get(resolvedThrownTypes.get(i))); + } + + return builder; + } + + public Builder toBuilder() { + Builder builder = new Builder(name); + builder.javadoc.add(javadoc); + builder.annotations.addAll(annotations); + builder.modifiers.addAll(modifiers); + builder.typeVariables.addAll(typeVariables); + builder.returnType = returnType; + builder.parameters.addAll(parameters); + builder.exceptions.addAll(exceptions); + builder.code.add(code); + builder.varargs = varargs; + builder.defaultValue = defaultValue; + return builder; + } + + public static final class Builder { + private final String name; + + private final CodeBlock.Builder javadoc = CodeBlock.builder(); + private final List<AnnotationSpec> annotations = new ArrayList<>(); + private final List<Modifier> modifiers = new ArrayList<>(); + private List<TypeVariableName> typeVariables = new ArrayList<>(); + private TypeName returnType; + private final List<ParameterSpec> parameters = new ArrayList<>(); + private final Set<TypeName> exceptions = new LinkedHashSet<>(); + private final CodeBlock.Builder code = CodeBlock.builder(); + private boolean varargs; + private CodeBlock defaultValue; + + private Builder(String name) { + checkNotNull(name, "name == null"); + checkArgument(name.equals(CONSTRUCTOR) || SourceVersion.isName(name), + "not a valid name: %s", name); + this.name = name; + this.returnType = name.equals(CONSTRUCTOR) ? null : TypeName.VOID; + } + + public Builder addJavadoc(String format, Object... args) { + javadoc.add(format, args); + return this; + } + + public Builder addJavadoc(CodeBlock block) { + javadoc.add(block); + return this; + } + + public Builder addAnnotations(Iterable<AnnotationSpec> annotationSpecs) { + checkArgument(annotationSpecs != null, "annotationSpecs == null"); + for (AnnotationSpec annotationSpec : annotationSpecs) { + this.annotations.add(annotationSpec); + } + return this; + } + + public Builder addAnnotation(AnnotationSpec annotationSpec) { + this.annotations.add(annotationSpec); + return this; + } + + public Builder addAnnotation(ClassName annotation) { + this.annotations.add(AnnotationSpec.builder(annotation).build()); + return this; + } + + public Builder addAnnotation(Class<?> annotation) { + return addAnnotation(ClassName.get(annotation)); + } + + public Builder addModifiers(Modifier... modifiers) { + checkNotNull(modifiers, "modifiers == null"); + Collections.addAll(this.modifiers, modifiers); + return this; + } + + public Builder addModifiers(Iterable<Modifier> modifiers) { + checkNotNull(modifiers, "modifiers == null"); + for (Modifier modifier : modifiers) { + this.modifiers.add(modifier); + } + return this; + } + + public Builder addTypeVariables(Iterable<TypeVariableName> typeVariables) { + checkArgument(typeVariables != null, "typeVariables == null"); + for (TypeVariableName typeVariable : typeVariables) { + this.typeVariables.add(typeVariable); + } + return this; + } + + public Builder addTypeVariable(TypeVariableName typeVariable) { + typeVariables.add(typeVariable); + return this; + } + + public Builder returns(TypeName returnType) { + checkState(!name.equals(CONSTRUCTOR), "constructor cannot have return type."); + this.returnType = returnType; + return this; + } + + public Builder returns(Type returnType) { + return returns(TypeName.get(returnType)); + } + + public Builder addParameters(Iterable<ParameterSpec> parameterSpecs) { + checkArgument(parameterSpecs != null, "parameterSpecs == null"); + for (ParameterSpec parameterSpec : parameterSpecs) { + this.parameters.add(parameterSpec); + } + return this; + } + + public Builder addParameter(ParameterSpec parameterSpec) { + this.parameters.add(parameterSpec); + return this; + } + + public Builder addParameter(TypeName type, String name, Modifier... modifiers) { + return addParameter(ParameterSpec.builder(type, name, modifiers).build()); + } + + public Builder addParameter(Type type, String name, Modifier... modifiers) { + return addParameter(TypeName.get(type), name, modifiers); + } + + public Builder varargs() { + return varargs(true); + } + + public Builder varargs(boolean varargs) { + this.varargs = varargs; + return this; + } + + public Builder addExceptions(Iterable<? extends TypeName> exceptions) { + checkArgument(exceptions != null, "exceptions == null"); + for (TypeName exception : exceptions) { + this.exceptions.add(exception); + } + return this; + } + + public Builder addException(TypeName exception) { + this.exceptions.add(exception); + return this; + } + + public Builder addException(Type exception) { + return addException(TypeName.get(exception)); + } + + public Builder addCode(String format, Object... args) { + code.add(format, args); + return this; + } + + public Builder addNamedCode(String format, Map<String, ?> args) { + code.addNamed(format, args); + return this; + } + + public Builder addCode(CodeBlock codeBlock) { + code.add(codeBlock); + return this; + } + + public Builder addComment(String format, Object... args) { + code.add("// " + format + "\n", args); + return this; + } + + public Builder defaultValue(String format, Object... args) { + return defaultValue(CodeBlock.of(format, args)); + } + + public Builder defaultValue(CodeBlock codeBlock) { + checkState(this.defaultValue == null, "defaultValue was already set"); + this.defaultValue = checkNotNull(codeBlock, "codeBlock == null"); + return this; + } + + /** + * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". + * Shouldn't contain braces or newline characters. + */ + public Builder beginControlFlow(String controlFlow, Object... args) { + code.beginControlFlow(controlFlow, args); + return this; + } + + /** + * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". + * Shouldn't contain braces or newline characters. + */ + public Builder nextControlFlow(String controlFlow, Object... args) { + code.nextControlFlow(controlFlow, args); + return this; + } + + public Builder endControlFlow() { + code.endControlFlow(); + return this; + } + + /** + * @param controlFlow the optional control flow construct and its code, such as + * "while(foo == 20)". Only used for "do/while" control flows. + */ + public Builder endControlFlow(String controlFlow, Object... args) { + code.endControlFlow(controlFlow, args); + return this; + } + + public Builder addStatement(String format, Object... args) { + code.addStatement(format, args); + return this; + } + + public Builder addStatement(CodeBlock codeBlock) { + code.addStatement(codeBlock); + return this; + } + + public MethodSpec build() { + return new MethodSpec(this); + } + } +} diff --git a/src/main/java/com/squareup/javapoet/NameAllocator.java b/src/main/java/com/squareup/javapoet/NameAllocator.java new file mode 100644 index 0000000..8269664 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/NameAllocator.java @@ -0,0 +1,168 @@ +/* + * 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.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import javax.lang.model.SourceVersion; + +import static com.squareup.javapoet.Util.checkNotNull; + +/** + * Assigns Java identifier names to avoid collisions, keywords, and invalid characters. To use, + * first create an instance and allocate all of the names that you need. Typically this is a + * mix of user-supplied names and constants: <pre> {@code + * + * NameAllocator nameAllocator = new NameAllocator(); + * for (MyProperty property : properties) { + * nameAllocator.newName(property.name(), property); + * } + * nameAllocator.newName("sb", "string builder"); + * }</pre> + * + * Pass a unique tag object to each allocation. The tag scopes the name, and can be used to look up + * the allocated name later. Typically the tag is the object that is being named. In the above + * example we use {@code property} for the user-supplied property names, and {@code "string + * builder"} for our constant string builder. + * + * <p>Once we've allocated names we can use them when generating code: <pre> {@code + * + * MethodSpec.Builder builder = MethodSpec.methodBuilder("toString") + * .addAnnotation(Override.class) + * .addModifiers(Modifier.PUBLIC) + * .returns(String.class); + * + * builder.addStatement("$1T $2N = new $1T()", + * StringBuilder.class, nameAllocator.get("string builder")); + * for (MyProperty property : properties) { + * builder.addStatement("$N.append($N)", + * nameAllocator.get("string builder"), nameAllocator.get(property)); + * } + * builder.addStatement("return $N", nameAllocator.get("string builder")); + * return builder.build(); + * }</pre> + * + * The above code generates unique names if presented with conflicts. Given user-supplied properties + * with names {@code ab} and {@code sb} this generates the following: <pre> {@code + * + * @Override + * public String toString() { + * StringBuilder sb_ = new StringBuilder(); + * sb_.append(ab); + * sb_.append(sb); + * return sb_.toString(); + * } + * }</pre> + * + * The underscore is appended to {@code sb} to avoid conflicting with the user-supplied {@code sb} + * property. Underscores are also prefixed for names that start with a digit, and used to replace + * name-unsafe characters like space or dash. + * + * <p>When dealing with multiple independent inner scopes, use a {@link #clone()} of the + * NameAllocator used for the outer scope to further refine name allocation for a specific inner + * scope. + */ +public final class NameAllocator implements Cloneable { + private final Set<String> allocatedNames; + private final Map<Object, String> tagToName; + + public NameAllocator() { + this(new LinkedHashSet<>(), new LinkedHashMap<>()); + } + + private NameAllocator(LinkedHashSet<String> allocatedNames, + LinkedHashMap<Object, String> tagToName) { + this.allocatedNames = allocatedNames; + this.tagToName = tagToName; + } + + /** + * Return a new name using {@code suggestion} that will not be a Java identifier or clash with + * other names. + */ + public String newName(String suggestion) { + return newName(suggestion, UUID.randomUUID().toString()); + } + + /** + * Return a new name using {@code suggestion} that will not be a Java identifier or clash with + * other names. The returned value can be queried multiple times by passing {@code tag} to + * {@link #get(Object)}. + */ + public String newName(String suggestion, Object tag) { + checkNotNull(suggestion, "suggestion"); + checkNotNull(tag, "tag"); + + suggestion = toJavaIdentifier(suggestion); + + while (SourceVersion.isKeyword(suggestion) || !allocatedNames.add(suggestion)) { + suggestion = suggestion + "_"; + } + + String replaced = tagToName.put(tag, suggestion); + if (replaced != null) { + tagToName.put(tag, replaced); // Put things back as they were! + throw new IllegalArgumentException("tag " + tag + " cannot be used for both '" + replaced + + "' and '" + suggestion + "'"); + } + + return suggestion; + } + + public static String toJavaIdentifier(String suggestion) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < suggestion.length(); ) { + int codePoint = suggestion.codePointAt(i); + if (i == 0 + && !Character.isJavaIdentifierStart(codePoint) + && Character.isJavaIdentifierPart(codePoint)) { + result.append("_"); + } + + int validCodePoint = Character.isJavaIdentifierPart(codePoint) ? codePoint : '_'; + result.appendCodePoint(validCodePoint); + i += Character.charCount(codePoint); + } + return result.toString(); + } + + /** Retrieve a name created with {@link #newName(String, Object)}. */ + public String get(Object tag) { + String result = tagToName.get(tag); + if (result == null) { + throw new IllegalArgumentException("unknown tag: " + tag); + } + return result; + } + + /** + * Create a deep copy of this NameAllocator. Useful to create multiple independent refinements + * of a NameAllocator to be used in the respective definition of multiples, independently-scoped, + * inner code blocks. + * + * @return A deep copy of this NameAllocator. + */ + @Override + public NameAllocator clone() { + return new NameAllocator( + new LinkedHashSet<>(this.allocatedNames), + new LinkedHashMap<>(this.tagToName)); + } + +} diff --git a/src/main/java/com/squareup/javapoet/ParameterSpec.java b/src/main/java/com/squareup/javapoet/ParameterSpec.java new file mode 100644 index 0000000..63da3f2 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/ParameterSpec.java @@ -0,0 +1,172 @@ +/* + * 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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; + +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; + +/** A generated parameter declaration. */ +public final class ParameterSpec { + public final String name; + public final List<AnnotationSpec> annotations; + public final Set<Modifier> modifiers; + public final TypeName type; + + private ParameterSpec(Builder builder) { + this.name = checkNotNull(builder.name, "name == null"); + this.annotations = Util.immutableList(builder.annotations); + this.modifiers = Util.immutableSet(builder.modifiers); + this.type = checkNotNull(builder.type, "type == null"); + } + + public boolean hasModifier(Modifier modifier) { + return modifiers.contains(modifier); + } + + void emit(CodeWriter codeWriter, boolean varargs) throws IOException { + codeWriter.emitAnnotations(annotations, true); + codeWriter.emitModifiers(modifiers); + if (varargs) { + TypeName.asArray(type).emit(codeWriter, true); + } else { + type.emit(codeWriter); + } + codeWriter.emit(" $L", name); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + StringBuilder out = new StringBuilder(); + try { + CodeWriter codeWriter = new CodeWriter(out); + emit(codeWriter, false); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public static ParameterSpec get(VariableElement element) { + TypeName type = TypeName.get(element.asType()); + String name = element.getSimpleName().toString(); + return ParameterSpec.builder(type, name) + .addModifiers(element.getModifiers()) + .build(); + } + + static List<ParameterSpec> parametersOf(ExecutableElement method) { + List<ParameterSpec> result = new ArrayList<>(); + for (VariableElement parameter : method.getParameters()) { + result.add(ParameterSpec.get(parameter)); + } + return result; + } + + public static Builder builder(TypeName type, String name, Modifier... modifiers) { + checkNotNull(type, "type == null"); + checkArgument(SourceVersion.isName(name), "not a valid name: %s", name); + return new Builder(type, name) + .addModifiers(modifiers); + } + + public static Builder builder(Type type, String name, Modifier... modifiers) { + return builder(TypeName.get(type), name, modifiers); + } + + public Builder toBuilder() { + return toBuilder(type, name); + } + + Builder toBuilder(TypeName type, String name) { + Builder builder = new Builder(type, name); + builder.annotations.addAll(annotations); + builder.modifiers.addAll(modifiers); + return builder; + } + + public static final class Builder { + private final TypeName type; + private final String name; + + private final List<AnnotationSpec> annotations = new ArrayList<>(); + private final List<Modifier> modifiers = new ArrayList<>(); + + private Builder(TypeName type, String name) { + this.type = type; + this.name = name; + } + + public Builder addAnnotations(Iterable<AnnotationSpec> annotationSpecs) { + checkArgument(annotationSpecs != null, "annotationSpecs == null"); + for (AnnotationSpec annotationSpec : annotationSpecs) { + this.annotations.add(annotationSpec); + } + return this; + } + + public Builder addAnnotation(AnnotationSpec annotationSpec) { + this.annotations.add(annotationSpec); + return this; + } + + public Builder addAnnotation(ClassName annotation) { + this.annotations.add(AnnotationSpec.builder(annotation).build()); + return this; + } + + public Builder addAnnotation(Class<?> annotation) { + return addAnnotation(ClassName.get(annotation)); + } + + public Builder addModifiers(Modifier... modifiers) { + Collections.addAll(this.modifiers, modifiers); + return this; + } + + public Builder addModifiers(Iterable<Modifier> modifiers) { + checkNotNull(modifiers, "modifiers == null"); + for (Modifier modifier : modifiers) { + this.modifiers.add(modifier); + } + return this; + } + + public ParameterSpec build() { + return new ParameterSpec(this); + } + } +} diff --git a/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java b/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java new file mode 100644 index 0000000..3a8bf62 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/ParameterizedTypeName.java @@ -0,0 +1,138 @@ +/* + * 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.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; + +public final class ParameterizedTypeName extends TypeName { + private final ParameterizedTypeName enclosingType; + public final ClassName rawType; + public final List<TypeName> typeArguments; + + ParameterizedTypeName(ParameterizedTypeName enclosingType, ClassName rawType, + List<TypeName> typeArguments) { + this(enclosingType, rawType, typeArguments, new ArrayList<>()); + } + + private ParameterizedTypeName(ParameterizedTypeName enclosingType, ClassName rawType, + List<TypeName> typeArguments, List<AnnotationSpec> annotations) { + super(annotations); + this.rawType = checkNotNull(rawType, "rawType == null").annotated(annotations); + this.enclosingType = enclosingType; + this.typeArguments = Util.immutableList(typeArguments); + + checkArgument(!this.typeArguments.isEmpty() || enclosingType != null, + "no type arguments: %s", rawType); + for (TypeName typeArgument : this.typeArguments) { + checkArgument(!typeArgument.isPrimitive() && typeArgument != VOID, + "invalid type parameter: %s", typeArgument); + } + } + + @Override public ParameterizedTypeName annotated(List<AnnotationSpec> annotations) { + return new ParameterizedTypeName( + enclosingType, rawType, typeArguments, concatAnnotations(annotations)); + } + + @Override + public TypeName withoutAnnotations() { + return new ParameterizedTypeName( + enclosingType, rawType.withoutAnnotations(), typeArguments, new ArrayList<>()); + } + + @Override CodeWriter emit(CodeWriter out) throws IOException { + if (enclosingType != null) { + enclosingType.emit(out); + out.emit("."); + if (isAnnotated()) { + out.emit(" "); + emitAnnotations(out); + } + out.emit(rawType.simpleName()); + } else { + rawType.emit(out); + } + if (!typeArguments.isEmpty()) { + out.emitAndIndent("<"); + boolean firstParameter = true; + for (TypeName parameter : typeArguments) { + if (!firstParameter) out.emitAndIndent(", "); + parameter.emit(out); + firstParameter = false; + } + out.emitAndIndent(">"); + } + return out; + } + + /** + * Returns a new {@link ParameterizedTypeName} instance for the specified {@code name} as nested + * inside this class. + */ + public ParameterizedTypeName nestedClass(String name) { + checkNotNull(name, "name == null"); + return new ParameterizedTypeName(this, rawType.nestedClass(name), new ArrayList<>(), + new ArrayList<>()); + } + + /** + * Returns a new {@link ParameterizedTypeName} instance for the specified {@code name} as nested + * inside this class, with the specified {@code typeArguments}. + */ + public ParameterizedTypeName nestedClass(String name, List<TypeName> typeArguments) { + checkNotNull(name, "name == null"); + return new ParameterizedTypeName(this, rawType.nestedClass(name), typeArguments, + new ArrayList<>()); + } + + /** Returns a parameterized type, applying {@code typeArguments} to {@code rawType}. */ + public static ParameterizedTypeName get(ClassName rawType, TypeName... typeArguments) { + return new ParameterizedTypeName(null, rawType, Arrays.asList(typeArguments)); + } + + /** Returns a parameterized type, applying {@code typeArguments} to {@code rawType}. */ + public static ParameterizedTypeName get(Class<?> rawType, Type... typeArguments) { + return new ParameterizedTypeName(null, ClassName.get(rawType), list(typeArguments)); + } + + /** Returns a parameterized type equivalent to {@code type}. */ + public static ParameterizedTypeName get(ParameterizedType type) { + return get(type, new LinkedHashMap<>()); + } + + /** Returns a parameterized type equivalent to {@code type}. */ + static ParameterizedTypeName get(ParameterizedType type, Map<Type, TypeVariableName> map) { + ClassName rawType = ClassName.get((Class<?>) type.getRawType()); + ParameterizedType ownerType = (type.getOwnerType() instanceof ParameterizedType) + && !Modifier.isStatic(((Class<?>) type.getRawType()).getModifiers()) + ? (ParameterizedType) type.getOwnerType() : null; + List<TypeName> typeArguments = TypeName.list(type.getActualTypeArguments(), map); + return (ownerType != null) + ? get(ownerType, map).nestedClass(rawType.simpleName(), typeArguments) + : new ParameterizedTypeName(null, rawType, typeArguments); + } +} diff --git a/src/main/java/com/squareup/javapoet/TypeName.java b/src/main/java/com/squareup/javapoet/TypeName.java new file mode 100644 index 0000000..38877f7 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/TypeName.java @@ -0,0 +1,385 @@ +/* + * 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.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleTypeVisitor8; + +/** + * Any type in Java's type system, plus {@code void}. This class is an identifier for primitive + * types like {@code int} and raw reference types like {@code String} and {@code List}. It also + * identifies composite types like {@code char[]} and {@code Set<Long>}. + * + * <p>Type names are dumb identifiers only and do not model the values they name. For example, the + * type name for {@code java.lang.List} doesn't know about the {@code size()} method, the fact that + * lists are collections, or even that it accepts a single type parameter. + * + * <p>Instances of this class are immutable value objects that implement {@code equals()} and {@code + * hashCode()} properly. + * + * <h3>Referencing existing types</h3> + * + * <p>Primitives and void are constants that you can reference directly: see {@link #INT}, {@link + * #DOUBLE}, and {@link #VOID}. + * + * <p>In an annotation processor you can get a type name instance for a type mirror by calling + * {@link #get(TypeMirror)}. In reflection code, you can use {@link #get(Type)}. + * + * <h3>Defining new types</h3> + * + * <p>Create new reference types like {@code com.example.HelloWorld} with {@link + * ClassName#get(String, String, String...)}. To build composite types like {@code char[]} and + * {@code Set<Long>}, use the factory methods on {@link ArrayTypeName}, {@link + * ParameterizedTypeName}, {@link TypeVariableName}, and {@link WildcardTypeName}. + */ +public class TypeName { + public static final TypeName VOID = new TypeName("void"); + public static final TypeName BOOLEAN = new TypeName("boolean"); + public static final TypeName BYTE = new TypeName("byte"); + public static final TypeName SHORT = new TypeName("short"); + public static final TypeName INT = new TypeName("int"); + public static final TypeName LONG = new TypeName("long"); + public static final TypeName CHAR = new TypeName("char"); + public static final TypeName FLOAT = new TypeName("float"); + public static final TypeName DOUBLE = new TypeName("double"); + public static final ClassName OBJECT = ClassName.get("java.lang", "Object"); + + private static final ClassName BOXED_VOID = ClassName.get("java.lang", "Void"); + private static final ClassName BOXED_BOOLEAN = ClassName.get("java.lang", "Boolean"); + private static final ClassName BOXED_BYTE = ClassName.get("java.lang", "Byte"); + private static final ClassName BOXED_SHORT = ClassName.get("java.lang", "Short"); + private static final ClassName BOXED_INT = ClassName.get("java.lang", "Integer"); + private static final ClassName BOXED_LONG = ClassName.get("java.lang", "Long"); + private static final ClassName BOXED_CHAR = ClassName.get("java.lang", "Character"); + private static final ClassName BOXED_FLOAT = ClassName.get("java.lang", "Float"); + private static final ClassName BOXED_DOUBLE = ClassName.get("java.lang", "Double"); + + /** The name of this type if it is a keyword, or null. */ + private final String keyword; + public final List<AnnotationSpec> annotations; + + /** Lazily-initialized toString of this type name. */ + private String cachedString; + + private TypeName(String keyword) { + this(keyword, new ArrayList<>()); + } + + private TypeName(String keyword, List<AnnotationSpec> annotations) { + this.keyword = keyword; + this.annotations = Util.immutableList(annotations); + } + + // Package-private constructor to prevent third-party subclasses. + TypeName(List<AnnotationSpec> annotations) { + this(null, annotations); + } + + public final TypeName annotated(AnnotationSpec... annotations) { + return annotated(Arrays.asList(annotations)); + } + + public TypeName annotated(List<AnnotationSpec> annotations) { + Util.checkNotNull(annotations, "annotations == null"); + return new TypeName(keyword, concatAnnotations(annotations)); + } + + public TypeName withoutAnnotations() { + return new TypeName(keyword); + } + + protected final List<AnnotationSpec> concatAnnotations(List<AnnotationSpec> annotations) { + List<AnnotationSpec> allAnnotations = new ArrayList<>(this.annotations); + allAnnotations.addAll(annotations); + return allAnnotations; + } + + public boolean isAnnotated() { + return !annotations.isEmpty(); + } + + /** + * Returns true if this is a primitive type like {@code int}. Returns false for all other types + * types including boxed primitives and {@code void}. + */ + public boolean isPrimitive() { + return keyword != null && this != VOID; + } + + /** + * Returns true if this is a boxed primitive type like {@code Integer}. Returns false for all + * other types types including unboxed primitives and {@code java.lang.Void}. + */ + public boolean isBoxedPrimitive() { + return this.equals(BOXED_BOOLEAN) + || this.equals(BOXED_BYTE) + || this.equals(BOXED_SHORT) + || this.equals(BOXED_INT) + || this.equals(BOXED_LONG) + || this.equals(BOXED_CHAR) + || this.equals(BOXED_FLOAT) + || this.equals(BOXED_DOUBLE); + } + + /** + * Returns a boxed type if this is a primitive type (like {@code Integer} for {@code int}) or + * {@code void}. Returns this type if boxing doesn't apply. + */ + public TypeName box() { + if (keyword == null) return this; // Doesn't need boxing. + if (this == VOID) return BOXED_VOID; + if (this == BOOLEAN) return BOXED_BOOLEAN; + if (this == BYTE) return BOXED_BYTE; + if (this == SHORT) return BOXED_SHORT; + if (this == INT) return BOXED_INT; + if (this == LONG) return BOXED_LONG; + if (this == CHAR) return BOXED_CHAR; + if (this == FLOAT) return BOXED_FLOAT; + if (this == DOUBLE) return BOXED_DOUBLE; + throw new AssertionError(keyword); + } + + /** + * Returns an unboxed type if this is a boxed primitive type (like {@code int} for {@code + * Integer}) or {@code Void}. Returns this type if it is already unboxed. + * + * @throws UnsupportedOperationException if this type isn't eligible for unboxing. + */ + public TypeName unbox() { + if (keyword != null) return this; // Already unboxed. + if (this.equals(BOXED_VOID)) return VOID; + if (this.equals(BOXED_BOOLEAN)) return BOOLEAN; + if (this.equals(BOXED_BYTE)) return BYTE; + if (this.equals(BOXED_SHORT)) return SHORT; + if (this.equals(BOXED_INT)) return INT; + if (this.equals(BOXED_LONG)) return LONG; + if (this.equals(BOXED_CHAR)) return CHAR; + if (this.equals(BOXED_FLOAT)) return FLOAT; + if (this.equals(BOXED_DOUBLE)) return DOUBLE; + throw new UnsupportedOperationException("cannot unbox " + this); + } + + @Override public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public final int hashCode() { + return toString().hashCode(); + } + + @Override public final String toString() { + String result = cachedString; + if (result == null) { + try { + StringBuilder resultBuilder = new StringBuilder(); + CodeWriter codeWriter = new CodeWriter(resultBuilder); + emit(codeWriter); + result = resultBuilder.toString(); + cachedString = result; + } catch (IOException e) { + throw new AssertionError(); + } + } + return result; + } + + CodeWriter emit(CodeWriter out) throws IOException { + if (keyword == null) throw new AssertionError(); + + if (isAnnotated()) { + out.emit(""); + emitAnnotations(out); + } + return out.emitAndIndent(keyword); + } + + CodeWriter emitAnnotations(CodeWriter out) throws IOException { + for (AnnotationSpec annotation : annotations) { + annotation.emit(out, true); + out.emit(" "); + } + return out; + } + + + /** Returns a type name equivalent to {@code mirror}. */ + public static TypeName get(TypeMirror mirror) { + return get(mirror, new LinkedHashMap<>()); + } + + static TypeName get(TypeMirror mirror, + final Map<TypeParameterElement, TypeVariableName> typeVariables) { + return mirror.accept(new SimpleTypeVisitor8<TypeName, Void>() { + @Override public TypeName visitPrimitive(PrimitiveType t, Void p) { + switch (t.getKind()) { + case BOOLEAN: + return TypeName.BOOLEAN; + case BYTE: + return TypeName.BYTE; + case SHORT: + return TypeName.SHORT; + case INT: + return TypeName.INT; + case LONG: + return TypeName.LONG; + case CHAR: + return TypeName.CHAR; + case FLOAT: + return TypeName.FLOAT; + case DOUBLE: + return TypeName.DOUBLE; + default: + throw new AssertionError(); + } + } + + @Override public TypeName visitDeclared(DeclaredType t, Void p) { + ClassName rawType = ClassName.get((TypeElement) t.asElement()); + TypeMirror enclosingType = t.getEnclosingType(); + TypeName enclosing = + (enclosingType.getKind() != TypeKind.NONE) + && !t.asElement().getModifiers().contains(Modifier.STATIC) + ? enclosingType.accept(this, null) + : null; + if (t.getTypeArguments().isEmpty() && !(enclosing instanceof ParameterizedTypeName)) { + return rawType; + } + + List<TypeName> typeArgumentNames = new ArrayList<>(); + for (TypeMirror mirror : t.getTypeArguments()) { + typeArgumentNames.add(get(mirror, typeVariables)); + } + return enclosing instanceof ParameterizedTypeName + ? ((ParameterizedTypeName) enclosing).nestedClass( + rawType.simpleName(), typeArgumentNames) + : new ParameterizedTypeName(null, rawType, typeArgumentNames); + } + + @Override public TypeName visitError(ErrorType t, Void p) { + return visitDeclared(t, p); + } + + @Override public ArrayTypeName visitArray(ArrayType t, Void p) { + return ArrayTypeName.get(t, typeVariables); + } + + @Override public TypeName visitTypeVariable(javax.lang.model.type.TypeVariable t, Void p) { + return TypeVariableName.get(t, typeVariables); + } + + @Override public TypeName visitWildcard(javax.lang.model.type.WildcardType t, Void p) { + return WildcardTypeName.get(t, typeVariables); + } + + @Override public TypeName visitNoType(NoType t, Void p) { + if (t.getKind() == TypeKind.VOID) return TypeName.VOID; + return super.visitUnknown(t, p); + } + + @Override protected TypeName defaultAction(TypeMirror e, Void p) { + throw new IllegalArgumentException("Unexpected type mirror: " + e); + } + }, null); + } + + /** Returns a type name equivalent to {@code type}. */ + public static TypeName get(Type type) { + return get(type, new LinkedHashMap<>()); + } + + static TypeName get(Type type, Map<Type, TypeVariableName> map) { + if (type instanceof Class<?>) { + Class<?> classType = (Class<?>) type; + if (type == void.class) return VOID; + if (type == boolean.class) return BOOLEAN; + if (type == byte.class) return BYTE; + if (type == short.class) return SHORT; + if (type == int.class) return INT; + if (type == long.class) return LONG; + if (type == char.class) return CHAR; + if (type == float.class) return FLOAT; + if (type == double.class) return DOUBLE; + if (classType.isArray()) return ArrayTypeName.of(get(classType.getComponentType(), map)); + return ClassName.get(classType); + + } else if (type instanceof ParameterizedType) { + return ParameterizedTypeName.get((ParameterizedType) type, map); + + } else if (type instanceof WildcardType) { + return WildcardTypeName.get((WildcardType) type, map); + + } else if (type instanceof TypeVariable<?>) { + return TypeVariableName.get((TypeVariable<?>) type, map); + + } else if (type instanceof GenericArrayType) { + return ArrayTypeName.get((GenericArrayType) type, map); + + } else { + throw new IllegalArgumentException("unexpected type: " + type); + } + } + + /** Converts an array of types to a list of type names. */ + static List<TypeName> list(Type[] types) { + return list(types, new LinkedHashMap<>()); + } + + static List<TypeName> list(Type[] types, Map<Type, TypeVariableName> map) { + List<TypeName> result = new ArrayList<>(types.length); + for (Type type : types) { + result.add(get(type, map)); + } + return result; + } + + /** Returns the array component of {@code type}, or null if {@code type} is not an array. */ + static TypeName arrayComponent(TypeName type) { + return type instanceof ArrayTypeName + ? ((ArrayTypeName) type).componentType + : null; + } + + /** Returns {@code type} as an array, or null if {@code type} is not an array. */ + static ArrayTypeName asArray(TypeName type) { + return type instanceof ArrayTypeName + ? ((ArrayTypeName) type) + : null; + } + +} diff --git a/src/main/java/com/squareup/javapoet/TypeSpec.java b/src/main/java/com/squareup/javapoet/TypeSpec.java new file mode 100644 index 0000000..46de3a5 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/TypeSpec.java @@ -0,0 +1,632 @@ +/* + * 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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +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.requireExactlyOneOf; + +/** A generated class, interface, or enum declaration. */ +public final class TypeSpec { + public final Kind kind; + public final String name; + public final CodeBlock anonymousTypeArguments; + public final CodeBlock javadoc; + public final List<AnnotationSpec> annotations; + public final Set<Modifier> modifiers; + public final List<TypeVariableName> typeVariables; + public final TypeName superclass; + public final List<TypeName> superinterfaces; + public final Map<String, TypeSpec> enumConstants; + public final List<FieldSpec> fieldSpecs; + public final CodeBlock staticBlock; + public final CodeBlock initializerBlock; + public final List<MethodSpec> methodSpecs; + public final List<TypeSpec> typeSpecs; + public final List<Element> originatingElements; + + private TypeSpec(Builder builder) { + this.kind = builder.kind; + this.name = builder.name; + this.anonymousTypeArguments = builder.anonymousTypeArguments; + this.javadoc = builder.javadoc.build(); + this.annotations = Util.immutableList(builder.annotations); + this.modifiers = Util.immutableSet(builder.modifiers); + this.typeVariables = Util.immutableList(builder.typeVariables); + this.superclass = builder.superclass; + this.superinterfaces = Util.immutableList(builder.superinterfaces); + this.enumConstants = Util.immutableMap(builder.enumConstants); + this.fieldSpecs = Util.immutableList(builder.fieldSpecs); + this.staticBlock = builder.staticBlock.build(); + this.initializerBlock = builder.initializerBlock.build(); + this.methodSpecs = Util.immutableList(builder.methodSpecs); + this.typeSpecs = Util.immutableList(builder.typeSpecs); + + List<Element> originatingElementsMutable = new ArrayList<>(); + originatingElementsMutable.addAll(builder.originatingElements); + for (TypeSpec typeSpec : builder.typeSpecs) { + originatingElementsMutable.addAll(typeSpec.originatingElements); + } + this.originatingElements = Util.immutableList(originatingElementsMutable); + } + + /** + * Creates a dummy type spec for type-resolution only (in CodeWriter) + * while emitting the type declaration but before entering the type body. + */ + private TypeSpec(TypeSpec type) { + assert type.anonymousTypeArguments == null; + this.kind = type.kind; + this.name = type.name; + this.anonymousTypeArguments = null; + this.javadoc = type.javadoc; + this.annotations = Collections.emptyList(); + this.modifiers = Collections.emptySet(); + this.typeVariables = Collections.emptyList(); + this.superclass = null; + this.superinterfaces = Collections.emptyList(); + this.enumConstants = Collections.emptyMap(); + this.fieldSpecs = Collections.emptyList(); + this.staticBlock = type.staticBlock; + this.initializerBlock = type.initializerBlock; + this.methodSpecs = Collections.emptyList(); + this.typeSpecs = Collections.emptyList(); + this.originatingElements = Collections.emptyList(); + } + + public boolean hasModifier(Modifier modifier) { + return modifiers.contains(modifier); + } + + public static Builder classBuilder(String name) { + return new Builder(Kind.CLASS, checkNotNull(name, "name == null"), null); + } + + public static Builder classBuilder(ClassName className) { + return classBuilder(checkNotNull(className, "className == null").simpleName()); + } + + public static Builder interfaceBuilder(String name) { + return new Builder(Kind.INTERFACE, checkNotNull(name, "name == null"), null); + } + + public static Builder interfaceBuilder(ClassName className) { + return interfaceBuilder(checkNotNull(className, "className == null").simpleName()); + } + + public static Builder enumBuilder(String name) { + return new Builder(Kind.ENUM, checkNotNull(name, "name == null"), null); + } + + public static Builder enumBuilder(ClassName className) { + return enumBuilder(checkNotNull(className, "className == null").simpleName()); + } + + public static Builder anonymousClassBuilder(String typeArgumentsFormat, Object... args) { + return anonymousClassBuilder(CodeBlock.builder() + .add(typeArgumentsFormat, args) + .build()); + } + + public static Builder anonymousClassBuilder(CodeBlock typeArguments) { + return new Builder(Kind.CLASS, null, typeArguments); + } + + public static Builder annotationBuilder(String name) { + return new Builder(Kind.ANNOTATION, checkNotNull(name, "name == null"), null); + } + + public static Builder annotationBuilder(ClassName className) { + return annotationBuilder(checkNotNull(className, "className == null").simpleName()); + } + + public Builder toBuilder() { + Builder builder = new Builder(kind, name, anonymousTypeArguments); + builder.javadoc.add(javadoc); + builder.annotations.addAll(annotations); + builder.modifiers.addAll(modifiers); + builder.typeVariables.addAll(typeVariables); + builder.superclass = superclass; + builder.superinterfaces.addAll(superinterfaces); + builder.enumConstants.putAll(enumConstants); + builder.fieldSpecs.addAll(fieldSpecs); + builder.methodSpecs.addAll(methodSpecs); + builder.typeSpecs.addAll(typeSpecs); + builder.initializerBlock.add(initializerBlock); + builder.staticBlock.add(staticBlock); + return builder; + } + + void emit(CodeWriter codeWriter, String enumName, Set<Modifier> implicitModifiers) + throws IOException { + // Nested classes interrupt wrapped line indentation. Stash the current wrapping state and put + // it back afterwards when this type is complete. + int previousStatementLine = codeWriter.statementLine; + codeWriter.statementLine = -1; + + try { + if (enumName != null) { + codeWriter.emitJavadoc(javadoc); + codeWriter.emitAnnotations(annotations, false); + codeWriter.emit("$L", enumName); + if (!anonymousTypeArguments.formatParts.isEmpty()) { + codeWriter.emit("("); + codeWriter.emit(anonymousTypeArguments); + codeWriter.emit(")"); + } + if (fieldSpecs.isEmpty() && methodSpecs.isEmpty() && typeSpecs.isEmpty()) { + return; // Avoid unnecessary braces "{}". + } + codeWriter.emit(" {\n"); + } else if (anonymousTypeArguments != null) { + TypeName supertype = !superinterfaces.isEmpty() ? superinterfaces.get(0) : superclass; + codeWriter.emit("new $T(", supertype); + codeWriter.emit(anonymousTypeArguments); + codeWriter.emit(") {\n"); + } else { + // Push an empty type (specifically without nested types) for type-resolution. + codeWriter.pushType(new TypeSpec(this)); + + codeWriter.emitJavadoc(javadoc); + codeWriter.emitAnnotations(annotations, false); + codeWriter.emitModifiers(modifiers, Util.union(implicitModifiers, kind.asMemberModifiers)); + if (kind == Kind.ANNOTATION) { + codeWriter.emit("$L $L", "@interface", name); + } else { + codeWriter.emit("$L $L", kind.name().toLowerCase(Locale.US), name); + } + codeWriter.emitTypeVariables(typeVariables); + + List<TypeName> extendsTypes; + List<TypeName> implementsTypes; + if (kind == Kind.INTERFACE) { + extendsTypes = superinterfaces; + implementsTypes = Collections.emptyList(); + } else { + extendsTypes = superclass.equals(ClassName.OBJECT) + ? Collections.emptyList() + : Collections.singletonList(superclass); + implementsTypes = superinterfaces; + } + + if (!extendsTypes.isEmpty()) { + codeWriter.emit(" extends"); + boolean firstType = true; + for (TypeName type : extendsTypes) { + if (!firstType) codeWriter.emit(","); + codeWriter.emit(" $T", type); + firstType = false; + } + } + + if (!implementsTypes.isEmpty()) { + codeWriter.emit(" implements"); + boolean firstType = true; + for (TypeName type : implementsTypes) { + if (!firstType) codeWriter.emit(","); + codeWriter.emit(" $T", type); + firstType = false; + } + } + + codeWriter.popType(); + + codeWriter.emit(" {\n"); + } + + codeWriter.pushType(this); + codeWriter.indent(); + boolean firstMember = true; + for (Iterator<Map.Entry<String, TypeSpec>> i = enumConstants.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry<String, TypeSpec> enumConstant = i.next(); + if (!firstMember) codeWriter.emit("\n"); + enumConstant.getValue().emit(codeWriter, enumConstant.getKey(), Collections.emptySet()); + firstMember = false; + if (i.hasNext()) { + codeWriter.emit(",\n"); + } else if (!fieldSpecs.isEmpty() || !methodSpecs.isEmpty() || !typeSpecs.isEmpty()) { + codeWriter.emit(";\n"); + } else { + codeWriter.emit("\n"); + } + } + + // Static fields. + for (FieldSpec fieldSpec : fieldSpecs) { + if (!fieldSpec.hasModifier(Modifier.STATIC)) continue; + if (!firstMember) codeWriter.emit("\n"); + fieldSpec.emit(codeWriter, kind.implicitFieldModifiers); + firstMember = false; + } + + if (!staticBlock.isEmpty()) { + if (!firstMember) codeWriter.emit("\n"); + codeWriter.emit(staticBlock); + firstMember = false; + } + + // Non-static fields. + for (FieldSpec fieldSpec : fieldSpecs) { + if (fieldSpec.hasModifier(Modifier.STATIC)) continue; + if (!firstMember) codeWriter.emit("\n"); + fieldSpec.emit(codeWriter, kind.implicitFieldModifiers); + firstMember = false; + } + + // Initializer block. + if (!initializerBlock.isEmpty()) { + if (!firstMember) codeWriter.emit("\n"); + codeWriter.emit(initializerBlock); + firstMember = false; + } + + // Constructors. + for (MethodSpec methodSpec : methodSpecs) { + if (!methodSpec.isConstructor()) continue; + if (!firstMember) codeWriter.emit("\n"); + methodSpec.emit(codeWriter, name, kind.implicitMethodModifiers); + firstMember = false; + } + + // Methods (static and non-static). + for (MethodSpec methodSpec : methodSpecs) { + if (methodSpec.isConstructor()) continue; + if (!firstMember) codeWriter.emit("\n"); + methodSpec.emit(codeWriter, name, kind.implicitMethodModifiers); + firstMember = false; + } + + // Types. + for (TypeSpec typeSpec : typeSpecs) { + if (!firstMember) codeWriter.emit("\n"); + typeSpec.emit(codeWriter, null, kind.implicitTypeModifiers); + firstMember = false; + } + + codeWriter.unindent(); + codeWriter.popType(); + + codeWriter.emit("}"); + if (enumName == null && anonymousTypeArguments == null) { + codeWriter.emit("\n"); // If this type isn't also a value, include a trailing newline. + } + } finally { + codeWriter.statementLine = previousStatementLine; + } + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (getClass() != o.getClass()) return false; + return toString().equals(o.toString()); + } + + @Override public int hashCode() { + return toString().hashCode(); + } + + @Override public String toString() { + StringBuilder out = new StringBuilder(); + try { + CodeWriter codeWriter = new CodeWriter(out); + emit(codeWriter, null, Collections.emptySet()); + return out.toString(); + } catch (IOException e) { + throw new AssertionError(); + } + } + + public enum Kind { + CLASS( + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet()), + + INTERFACE( + Util.immutableSet(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)), + Util.immutableSet(Arrays.asList(Modifier.PUBLIC, Modifier.ABSTRACT)), + Util.immutableSet(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC)), + Util.immutableSet(Collections.singletonList(Modifier.STATIC))), + + ENUM( + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet(), + Collections.singleton(Modifier.STATIC)), + + ANNOTATION( + Util.immutableSet(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)), + Util.immutableSet(Arrays.asList(Modifier.PUBLIC, Modifier.ABSTRACT)), + Util.immutableSet(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC)), + Util.immutableSet(Collections.singletonList(Modifier.STATIC))); + + private final Set<Modifier> implicitFieldModifiers; + private final Set<Modifier> implicitMethodModifiers; + private final Set<Modifier> implicitTypeModifiers; + private final Set<Modifier> asMemberModifiers; + + Kind(Set<Modifier> implicitFieldModifiers, + Set<Modifier> implicitMethodModifiers, + Set<Modifier> implicitTypeModifiers, + Set<Modifier> asMemberModifiers) { + this.implicitFieldModifiers = implicitFieldModifiers; + this.implicitMethodModifiers = implicitMethodModifiers; + this.implicitTypeModifiers = implicitTypeModifiers; + this.asMemberModifiers = asMemberModifiers; + } + } + + public static final class Builder { + private final Kind kind; + private final String name; + private final CodeBlock anonymousTypeArguments; + + private final CodeBlock.Builder javadoc = CodeBlock.builder(); + private final List<AnnotationSpec> annotations = new ArrayList<>(); + private final List<Modifier> modifiers = new ArrayList<>(); + private final List<TypeVariableName> typeVariables = new ArrayList<>(); + private TypeName superclass = ClassName.OBJECT; + private final List<TypeName> superinterfaces = new ArrayList<>(); + private final Map<String, TypeSpec> enumConstants = new LinkedHashMap<>(); + private final List<FieldSpec> fieldSpecs = new ArrayList<>(); + private final CodeBlock.Builder staticBlock = CodeBlock.builder(); + private final CodeBlock.Builder initializerBlock = CodeBlock.builder(); + private final List<MethodSpec> methodSpecs = new ArrayList<>(); + private final List<TypeSpec> typeSpecs = new ArrayList<>(); + private final List<Element> originatingElements = new ArrayList<>(); + + private Builder(Kind kind, String name, + CodeBlock anonymousTypeArguments) { + checkArgument(name == null || SourceVersion.isName(name), "not a valid name: %s", name); + this.kind = kind; + this.name = name; + this.anonymousTypeArguments = anonymousTypeArguments; + } + + public Builder addJavadoc(String format, Object... args) { + javadoc.add(format, args); + return this; + } + + public Builder addJavadoc(CodeBlock block) { + javadoc.add(block); + return this; + } + + public Builder addAnnotations(Iterable<AnnotationSpec> annotationSpecs) { + checkArgument(annotationSpecs != null, "annotationSpecs == null"); + for (AnnotationSpec annotationSpec : annotationSpecs) { + this.annotations.add(annotationSpec); + } + return this; + } + + public Builder addAnnotation(AnnotationSpec annotationSpec) { + checkNotNull(annotationSpec, "annotationSpec == null"); + this.annotations.add(annotationSpec); + return this; + } + + public Builder addAnnotation(ClassName annotation) { + return addAnnotation(AnnotationSpec.builder(annotation).build()); + } + + public Builder addAnnotation(Class<?> annotation) { + return addAnnotation(ClassName.get(annotation)); + } + + public Builder addModifiers(Modifier... modifiers) { + checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); + for (Modifier modifier : modifiers) { + checkArgument(modifier != null, "modifiers contain null"); + this.modifiers.add(modifier); + } + return this; + } + + public Builder addTypeVariables(Iterable<TypeVariableName> typeVariables) { + checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); + checkArgument(typeVariables != null, "typeVariables == null"); + for (TypeVariableName typeVariable : typeVariables) { + this.typeVariables.add(typeVariable); + } + return this; + } + + public Builder addTypeVariable(TypeVariableName typeVariable) { + checkState(anonymousTypeArguments == null, "forbidden on anonymous types."); + typeVariables.add(typeVariable); + return this; + } + + public Builder superclass(TypeName superclass) { + checkState(this.kind == Kind.CLASS, "only classes have super classes, not " + this.kind); + checkState(this.superclass == ClassName.OBJECT, + "superclass already set to " + this.superclass); + checkArgument(!superclass.isPrimitive(), "superclass may not be a primitive"); + this.superclass = superclass; + return this; + } + + public Builder superclass(Type superclass) { + return superclass(TypeName.get(superclass)); + } + + public Builder addSuperinterfaces(Iterable<? extends TypeName> superinterfaces) { + checkArgument(superinterfaces != null, "superinterfaces == null"); + for (TypeName superinterface : superinterfaces) { + addSuperinterface(superinterface); + } + return this; + } + + public Builder addSuperinterface(TypeName superinterface) { + checkArgument(superinterface != null, "superinterface == null"); + this.superinterfaces.add(superinterface); + return this; + } + + public Builder addSuperinterface(Type superinterface) { + return addSuperinterface(TypeName.get(superinterface)); + } + + public Builder addEnumConstant(String name) { + return addEnumConstant(name, anonymousClassBuilder("").build()); + } + + public Builder addEnumConstant(String name, TypeSpec typeSpec) { + checkState(kind == Kind.ENUM, "%s is not enum", this.name); + checkArgument(typeSpec.anonymousTypeArguments != null, + "enum constants must have anonymous type arguments"); + checkArgument(SourceVersion.isName(name), "not a valid enum constant: %s", name); + enumConstants.put(name, typeSpec); + return this; + } + + public Builder addFields(Iterable<FieldSpec> fieldSpecs) { + checkArgument(fieldSpecs != null, "fieldSpecs == null"); + for (FieldSpec fieldSpec : fieldSpecs) { + addField(fieldSpec); + } + return this; + } + + public Builder addField(FieldSpec fieldSpec) { + if (kind == Kind.INTERFACE || kind == Kind.ANNOTATION) { + requireExactlyOneOf(fieldSpec.modifiers, Modifier.PUBLIC, Modifier.PRIVATE); + Set<Modifier> check = EnumSet.of(Modifier.STATIC, Modifier.FINAL); + checkState(fieldSpec.modifiers.containsAll(check), "%s %s.%s requires modifiers %s", + kind, name, fieldSpec.name, check); + } + fieldSpecs.add(fieldSpec); + return this; + } + + public Builder addField(TypeName type, String name, Modifier... modifiers) { + return addField(FieldSpec.builder(type, name, modifiers).build()); + } + + public Builder addField(Type type, String name, Modifier... modifiers) { + return addField(TypeName.get(type), name, modifiers); + } + + public Builder addStaticBlock(CodeBlock block) { + staticBlock.beginControlFlow("static").add(block).endControlFlow(); + return this; + } + + public Builder addInitializerBlock(CodeBlock block) { + if ((kind != Kind.CLASS && kind != Kind.ENUM)) { + throw new UnsupportedOperationException(kind + " can't have initializer blocks"); + } + initializerBlock.add("{\n") + .indent() + .add(block) + .unindent() + .add("}\n"); + return this; + } + + public Builder addMethods(Iterable<MethodSpec> methodSpecs) { + checkArgument(methodSpecs != null, "methodSpecs == null"); + for (MethodSpec methodSpec : methodSpecs) { + addMethod(methodSpec); + } + return this; + } + + public Builder addMethod(MethodSpec methodSpec) { + if (kind == Kind.INTERFACE) { + requireExactlyOneOf(methodSpec.modifiers, Modifier.ABSTRACT, Modifier.STATIC, + Modifier.DEFAULT); + requireExactlyOneOf(methodSpec.modifiers, Modifier.PUBLIC, Modifier.PRIVATE); + } else if (kind == Kind.ANNOTATION) { + checkState(methodSpec.modifiers.equals(kind.implicitMethodModifiers), + "%s %s.%s requires modifiers %s", + kind, name, methodSpec.name, kind.implicitMethodModifiers); + } + if (kind != Kind.ANNOTATION) { + checkState(methodSpec.defaultValue == null, "%s %s.%s cannot have a default value", + kind, name, methodSpec.name); + } + if (kind != Kind.INTERFACE) { + checkState(!methodSpec.hasModifier(Modifier.DEFAULT), "%s %s.%s cannot be default", + kind, name, methodSpec.name); + } + methodSpecs.add(methodSpec); + return this; + } + + public Builder addTypes(Iterable<TypeSpec> typeSpecs) { + checkArgument(typeSpecs != null, "typeSpecs == null"); + for (TypeSpec typeSpec : typeSpecs) { + addType(typeSpec); + } + return this; + } + + public Builder addType(TypeSpec typeSpec) { + checkArgument(typeSpec.modifiers.containsAll(kind.implicitTypeModifiers), + "%s %s.%s requires modifiers %s", kind, name, typeSpec.name, + kind.implicitTypeModifiers); + typeSpecs.add(typeSpec); + return this; + } + + public Builder addOriginatingElement(Element originatingElement) { + originatingElements.add(originatingElement); + return this; + } + + public TypeSpec build() { + checkArgument(kind != Kind.ENUM || !enumConstants.isEmpty(), + "at least one enum constant is required for %s", name); + + boolean isAbstract = modifiers.contains(Modifier.ABSTRACT) || kind != Kind.CLASS; + for (MethodSpec methodSpec : methodSpecs) { + checkArgument(isAbstract || !methodSpec.hasModifier(Modifier.ABSTRACT), + "non-abstract type %s cannot declare abstract method %s", name, methodSpec.name); + } + + boolean superclassIsObject = superclass.equals(ClassName.OBJECT); + int interestingSupertypeCount = (superclassIsObject ? 0 : 1) + superinterfaces.size(); + checkArgument(anonymousTypeArguments == null || interestingSupertypeCount <= 1, + "anonymous type has too many supertypes"); + + return new TypeSpec(this); + } + } +} diff --git a/src/main/java/com/squareup/javapoet/TypeVariableName.java b/src/main/java/com/squareup/javapoet/TypeVariableName.java new file mode 100644 index 0000000..54c2fa5 --- /dev/null +++ b/src/main/java/com/squareup/javapoet/TypeVariableName.java @@ -0,0 +1,168 @@ +/* + * 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.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import static com.squareup.javapoet.Util.checkArgument; +import static com.squareup.javapoet.Util.checkNotNull; + +public final class TypeVariableName extends TypeName { + public final String name; + public final List<TypeName> bounds; + + private TypeVariableName(String name, List<TypeName> bounds) { + this(name, bounds, new ArrayList<>()); + } + + private TypeVariableName(String name, List<TypeName> bounds, List<AnnotationSpec> annotations) { + super(annotations); + this.name = checkNotNull(name, "name == null"); + this.bounds = bounds; + + for (TypeName bound : this.bounds) { + checkArgument(!bound.isPrimitive() && bound != VOID, "invalid bound: %s", bound); + } + } + + @Override public TypeVariableName annotated(List<AnnotationSpec> annotations) { + return new TypeVariableName(name, bounds, annotations); + } + + @Override public TypeName withoutAnnotations() { + return new TypeVariableName(name, bounds); + } + + public TypeVariableName withBounds(Type... bounds) { + return withBounds(TypeName.list(bounds)); + } + + public TypeVariableName withBounds(TypeName... bounds) { + return withBounds(Arrays.asList(bounds)); + } + + public TypeVariableName withBounds(List<? extends TypeName> bounds) { + ArrayList<TypeName> newBounds = new ArrayList<>(); + newBounds.addAll(this.bounds); + newBounds.addAll(bounds); + return new TypeVariableName(name, newBounds, annotations); + } + + private static TypeVariableName of(String name, List<TypeName> bounds) { + // Strip java.lang.Object from bounds if it is present. + List<TypeName> boundsNoObject = new ArrayList<>(bounds); + boundsNoObject.remove(OBJECT); + return new TypeVariableName(name, Collections.unmodifiableList(boundsNoObject)); + } + + @Override CodeWriter emit(CodeWriter out) throws IOException { + emitAnnotations(out); + return out.emitAndIndent(name); + } + + /** Returns type variable named {@code name} without bounds. */ + public static TypeVariableName get(String name) { + return TypeVariableName.of(name, Collections.emptyList()); + } + + /** Returns type variable named {@code name} with {@code bounds}. */ + public static TypeVariableName get(String name, TypeName... bounds) { + return TypeVariableName.of(name, Arrays.asList(bounds)); + } + + /** Returns type variable named {@code name} with {@code bounds}. */ + public static TypeVariableName get(String name, Type... bounds) { + return TypeVariableName.of(name, TypeName.list(bounds)); + } + + /** Returns type variable equivalent to {@code mirror}. */ + public static TypeVariableName get(TypeVariable mirror) { + return get((TypeParameterElement) mirror.asElement()); + } + + /** + * Make a TypeVariableName for the given TypeMirror. This form is used internally to avoid + * infinite recursion in cases like {@code Enum<E extends Enum<E>>}. When we encounter such a + * thing, we will make a TypeVariableName without bounds and add that to the {@code typeVariables} + * map before looking up the bounds. Then if we encounter this TypeVariable again while + * constructing the bounds, we can just return it from the map. And, the code that put the entry + * in {@code variables} will make sure that the bounds are filled in before returning. + */ + static TypeVariableName get( + TypeVariable mirror, Map<TypeParameterElement, TypeVariableName> typeVariables) { + TypeParameterElement element = (TypeParameterElement) mirror.asElement(); + TypeVariableName typeVariableName = typeVariables.get(element); + if (typeVariableName == null) { + // Since the bounds field is public, we need to make it an unmodifiableList. But we control + // the List that that wraps, which means we can change it before returning. + List<TypeName> bounds = new ArrayList<>(); + List<TypeName> visibleBounds = Collections.unmodifiableList(bounds); + typeVariableName = new TypeVariableName(element.getSimpleName().toString(), visibleBounds); + typeVariables.put(element, typeVariableName); + for (TypeMirror typeMirror : element.getBounds()) { + bounds.add(TypeName.get(typeMirror, typeVariables)); + } + bounds.remove(OBJECT); + } + return typeVariableName; + } + + /** Returns type variable equivalent to {@code element}. */ + public static TypeVariableName get(TypeParameterElement element) { + String name = element.getSimpleName().toString(); + List<? extends TypeMirror> boundsMirrors = element.getBounds(); + + List<TypeName> boundsTypeNames = new ArrayList<>(); + for (TypeMirror typeMirror : boundsMirrors) { + boundsTypeNames.add(TypeName.get(typeMirror)); + } + + return TypeVariableName.of(name, boundsTypeNames); + } + + /** Returns type variable equivalent to {@code type}. */ + public static TypeVariableName get(java.lang.reflect.TypeVariable<?> type) { + return get(type, new LinkedHashMap<>()); + } + + /** @see #get(java.lang.reflect.TypeVariable, Map) */ + static TypeVariableName get(java.lang.reflect.TypeVariable<?> type, + Map<Type, TypeVariableName> map) { + TypeVariableName result = map.get(type); + if (result == null) { + List<TypeName> bounds = new ArrayList<>(); + List<TypeName> visibleBounds = Collections.unmodifiableList(bounds); + result = new TypeVariableName(type.getName(), visibleBounds); + map.put(type, result); + for (Type bound : type.getBounds()) { + bounds.add(TypeName.get(bound, map)); + } + bounds.remove(OBJECT); + } + return result; + } +} diff --git a/src/main/java/com/squareup/javapoet/Util.java b/src/main/java/com/squareup/javapoet/Util.java new file mode 100644 index 0000000..e0eabad --- /dev/null +++ b/src/main/java/com/squareup/javapoet/Util.java @@ -0,0 +1,131 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.lang.model.element.Modifier; + +import static java.lang.Character.isISOControl; + +/** + * Like Guava, but worse and standalone. This makes it easier to mix JavaPoet with libraries that + * bring their own version of Guava. + */ +final class Util { + private Util() { + } + + static <K, V> Map<K, List<V>> immutableMultimap(Map<K, List<V>> multimap) { + LinkedHashMap<K, List<V>> result = new LinkedHashMap<>(); + for (Map.Entry<K, List<V>> entry : multimap.entrySet()) { + if (entry.getValue().isEmpty()) continue; + result.put(entry.getKey(), immutableList(entry.getValue())); + } + return Collections.unmodifiableMap(result); + } + + static <K, V> Map<K, V> immutableMap(Map<K, V> map) { + return Collections.unmodifiableMap(new LinkedHashMap<>(map)); + } + + static void checkArgument(boolean condition, String format, Object... args) { + if (!condition) throw new IllegalArgumentException(String.format(format, args)); + } + + static <T> T checkNotNull(T reference, String format, Object... args) { + if (reference == null) throw new NullPointerException(String.format(format, args)); + return reference; + } + + static void checkState(boolean condition, String format, Object... args) { + if (!condition) throw new IllegalStateException(String.format(format, args)); + } + + static <T> List<T> immutableList(Collection<T> collection) { + return Collections.unmodifiableList(new ArrayList<>(collection)); + } + + static <T> Set<T> immutableSet(Collection<T> set) { + return Collections.unmodifiableSet(new LinkedHashSet<>(set)); + } + + static <T> Set<T> union(Set<T> a, Set<T> b) { + Set<T> result = new LinkedHashSet<>(); + result.addAll(a); + result.addAll(b); + return result; + } + + static void requireExactlyOneOf(Set<Modifier> modifiers, Modifier... mutuallyExclusive) { + int count = 0; + for (Modifier modifier : mutuallyExclusive) { + if (modifiers.contains(modifier)) count++; + } + checkArgument(count == 1, "modifiers %s must contain one of %s", + modifiers, Arrays.toString(mutuallyExclusive)); + } + + static String characterLiteralWithoutSingleQuotes(char c) { + // see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6 + switch (c) { + case '\b': return "\\b"; /* \u0008: backspace (BS) */ + case '\t': return "\\t"; /* \u0009: horizontal tab (HT) */ + case '\n': return "\\n"; /* \u000a: linefeed (LF) */ + case '\f': return "\\f"; /* \u000c: form feed (FF) */ + case '\r': return "\\r"; /* \u000d: carriage return (CR) */ + case '\"': return "\""; /* \u0022: double quote (") */ + case '\'': return "\\'"; /* \u0027: single quote (') */ + case '\\': return "\\\\"; /* \u005c: backslash (\) */ + default: + return isISOControl(c) ? String.format("\\u%04x", (int) c) : Character.toString(c); + } + } + + /** Returns the string literal representing {@code value}, including wrapping double quotes. */ + static String stringLiteralWithDoubleQuotes(String value, String indent) { + StringBuilder result = new StringBuilder(value.length() + 2); + result.append('"'); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + // trivial case: single quote must not be escaped + if (c == '\'') { + result.append("'"); + continue; + } + // trivial case: double quotes must be escaped + if (c == '\"') { + result.append("\\\""); + continue; + } + // default case: just let character literal do its work + result.append(characterLiteralWithoutSingleQuotes(c)); + // need to append indent after linefeed? + if (c == '\n' && i + 1 < value.length()) { + result.append("\"\n").append(indent).append(indent).append("+ \""); + } + } + result.append('"'); + return result.toString(); + } +} diff --git a/src/main/java/com/squareup/javapoet/WildcardTypeName.java b/src/main/java/com/squareup/javapoet/WildcardTypeName.java new file mode 100644 index 0000000..17cb73f --- /dev/null +++ b/src/main/java/com/squareup/javapoet/WildcardTypeName.java @@ -0,0 +1,129 @@ +/* + * 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.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; + +import static com.squareup.javapoet.Util.checkArgument; + +public final class WildcardTypeName extends TypeName { + public final List<TypeName> upperBounds; + public final List<TypeName> lowerBounds; + + private WildcardTypeName(List<TypeName> upperBounds, List<TypeName> lowerBounds) { + this(upperBounds, lowerBounds, new ArrayList<>()); + } + + private WildcardTypeName(List<TypeName> upperBounds, List<TypeName> lowerBounds, + List<AnnotationSpec> annotations) { + super(annotations); + this.upperBounds = Util.immutableList(upperBounds); + this.lowerBounds = Util.immutableList(lowerBounds); + + checkArgument(this.upperBounds.size() == 1, "unexpected extends bounds: %s", upperBounds); + for (TypeName upperBound : this.upperBounds) { + checkArgument(!upperBound.isPrimitive() && upperBound != VOID, + "invalid upper bound: %s", upperBound); + } + for (TypeName lowerBound : this.lowerBounds) { + checkArgument(!lowerBound.isPrimitive() && lowerBound != VOID, + "invalid lower bound: %s", lowerBound); + } + } + + @Override public WildcardTypeName annotated(List<AnnotationSpec> annotations) { + return new WildcardTypeName(upperBounds, lowerBounds, concatAnnotations(annotations)); + } + + @Override public TypeName withoutAnnotations() { + return new WildcardTypeName(upperBounds, lowerBounds); + } + + @Override CodeWriter emit(CodeWriter out) throws IOException { + if (lowerBounds.size() == 1) { + return out.emit("? super $T", lowerBounds.get(0)); + } + return upperBounds.get(0).equals(TypeName.OBJECT) + ? out.emit("?") + : out.emit("? extends $T", upperBounds.get(0)); + } + + /** + * Returns a type that represents an unknown type that extends {@code bound}. For example, if + * {@code bound} is {@code CharSequence.class}, this returns {@code ? extends CharSequence}. If + * {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code + * ? extends Object}. + */ + public static WildcardTypeName subtypeOf(TypeName upperBound) { + return new WildcardTypeName(Collections.singletonList(upperBound), Collections.emptyList()); + } + + public static WildcardTypeName subtypeOf(Type upperBound) { + return subtypeOf(TypeName.get(upperBound)); + } + + /** + * Returns a type that represents an unknown supertype of {@code bound}. For example, if {@code + * bound} is {@code String.class}, this returns {@code ? super String}. + */ + public static WildcardTypeName supertypeOf(TypeName lowerBound) { + return new WildcardTypeName(Collections.singletonList(OBJECT), + Collections.singletonList(lowerBound)); + } + + public static WildcardTypeName supertypeOf(Type lowerBound) { + return supertypeOf(TypeName.get(lowerBound)); + } + + public static TypeName get(javax.lang.model.type.WildcardType mirror) { + return get(mirror, new LinkedHashMap<>()); + } + + static TypeName get( + javax.lang.model.type.WildcardType mirror, + Map<TypeParameterElement, TypeVariableName> typeVariables) { + TypeMirror extendsBound = mirror.getExtendsBound(); + if (extendsBound == null) { + TypeMirror superBound = mirror.getSuperBound(); + if (superBound == null) { + return subtypeOf(Object.class); + } else { + return supertypeOf(TypeName.get(superBound, typeVariables)); + } + } else { + return subtypeOf(TypeName.get(extendsBound, typeVariables)); + } + } + + public static TypeName get(WildcardType wildcardName) { + return get(wildcardName, new LinkedHashMap<>()); + } + + static TypeName get(WildcardType wildcardName, Map<Type, TypeVariableName> map) { + return new WildcardTypeName( + list(wildcardName.getUpperBounds(), map), + list(wildcardName.getLowerBounds(), map)); + } +} diff --git a/src/test/java/com/squareup/javapoet/AbstractTypesTest.java b/src/test/java/com/squareup/javapoet/AbstractTypesTest.java new file mode 100644 index 0000000..86d9cbc --- /dev/null +++ b/src/test/java/com/squareup/javapoet/AbstractTypesTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2014 Google, 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 static com.google.common.truth.Truth.assertThat; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import static javax.lang.model.util.ElementFilter.fieldsIn; +import static org.junit.Assert.*; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +import org.junit.Test; + +public abstract class AbstractTypesTest { + protected abstract Elements getElements(); + protected abstract Types getTypes(); + + private TypeElement getElement(Class<?> clazz) { + return getElements().getTypeElement(clazz.getCanonicalName()); + } + + private TypeMirror getMirror(Class<?> clazz) { + return getElement(clazz).asType(); + } + + @Test public void getBasicTypeMirror() { + assertThat(TypeName.get(getMirror(Object.class))) + .isEqualTo(ClassName.get(Object.class)); + assertThat(TypeName.get(getMirror(Charset.class))) + .isEqualTo(ClassName.get(Charset.class)); + assertThat(TypeName.get(getMirror(AbstractTypesTest.class))) + .isEqualTo(ClassName.get(AbstractTypesTest.class)); + } + + @Test public void getParameterizedTypeMirror() { + DeclaredType setType = + getTypes().getDeclaredType(getElement(Set.class), getMirror(Object.class)); + assertThat(TypeName.get(setType)) + .isEqualTo(ParameterizedTypeName.get(ClassName.get(Set.class), ClassName.OBJECT)); + } + + @Test public void errorTypes() { + JavaFileObject hasErrorTypes = + JavaFileObjects.forSourceLines( + "com.squareup.tacos.ErrorTypes", + "package com.squareup.tacos;", + "", + "@SuppressWarnings(\"hook-into-compiler\")", + "class ErrorTypes {", + " Tacos tacos;", + " Ingredients.Guacamole guacamole;", + "}"); + Compilation compilation = javac().withProcessors(new AbstractProcessor() { + @Override + public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { + TypeElement classFile = + processingEnv.getElementUtils().getTypeElement("com.squareup.tacos.ErrorTypes"); + List<VariableElement> fields = fieldsIn(classFile.getEnclosedElements()); + ErrorType topLevel = (ErrorType) fields.get(0).asType(); + ErrorType member = (ErrorType) fields.get(1).asType(); + + assertThat(TypeName.get(topLevel)).isEqualTo(ClassName.get("", "Tacos")); + assertThat(TypeName.get(member)).isEqualTo(ClassName.get("Ingredients", "Guacamole")); + return false; + } + + @Override + public Set<String> getSupportedAnnotationTypes() { + return Collections.singleton("*"); + } + }).compile(hasErrorTypes); + + assertThat(compilation).failed(); + } + + static class Parameterized< + Simple, + ExtendsClass extends Number, + ExtendsInterface extends Runnable, + ExtendsTypeVariable extends Simple, + Intersection extends Number & Runnable, + IntersectionOfInterfaces extends Runnable & Serializable> {} + + @Test public void getTypeVariableTypeMirror() { + List<? extends TypeParameterElement> typeVariables = + getElement(Parameterized.class).getTypeParameters(); + + // Members of converted types use ClassName and not Class<?>. + ClassName number = ClassName.get(Number.class); + ClassName runnable = ClassName.get(Runnable.class); + ClassName serializable = ClassName.get(Serializable.class); + + assertThat(TypeName.get(typeVariables.get(0).asType())) + .isEqualTo(TypeVariableName.get("Simple")); + assertThat(TypeName.get(typeVariables.get(1).asType())) + .isEqualTo(TypeVariableName.get("ExtendsClass", number)); + assertThat(TypeName.get(typeVariables.get(2).asType())) + .isEqualTo(TypeVariableName.get("ExtendsInterface", runnable)); + assertThat(TypeName.get(typeVariables.get(3).asType())) + .isEqualTo(TypeVariableName.get("ExtendsTypeVariable", TypeVariableName.get("Simple"))); + assertThat(TypeName.get(typeVariables.get(4).asType())) + .isEqualTo(TypeVariableName.get("Intersection", number, runnable)); + assertThat(TypeName.get(typeVariables.get(5).asType())) + .isEqualTo(TypeVariableName.get("IntersectionOfInterfaces", runnable, serializable)); + assertThat(((TypeVariableName) TypeName.get(typeVariables.get(4).asType())).bounds) + .containsExactly(number, runnable); + } + + static class Recursive<T extends Map<List<T>, Set<T[]>>> {} + + @Test + public void getTypeVariableTypeMirrorRecursive() { + TypeMirror typeMirror = getElement(Recursive.class).asType(); + ParameterizedTypeName typeName = (ParameterizedTypeName) TypeName.get(typeMirror); + String className = Recursive.class.getCanonicalName(); + assertThat(typeName.toString()).isEqualTo(className + "<T>"); + + TypeVariableName typeVariableName = (TypeVariableName) typeName.typeArguments.get(0); + + try { + typeVariableName.bounds.set(0, null); + fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + } + + assertThat(typeVariableName.toString()).isEqualTo("T"); + assertThat(typeVariableName.bounds.toString()) + .isEqualTo("[java.util.Map<java.util.List<T>, java.util.Set<T[]>>]"); + } + + @Test public void getPrimitiveTypeMirror() { + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.BOOLEAN))) + .isEqualTo(TypeName.BOOLEAN); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.BYTE))) + .isEqualTo(TypeName.BYTE); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.SHORT))) + .isEqualTo(TypeName.SHORT); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.INT))) + .isEqualTo(TypeName.INT); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.LONG))) + .isEqualTo(TypeName.LONG); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.CHAR))) + .isEqualTo(TypeName.CHAR); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.FLOAT))) + .isEqualTo(TypeName.FLOAT); + assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.DOUBLE))) + .isEqualTo(TypeName.DOUBLE); + } + + @Test public void getArrayTypeMirror() { + assertThat(TypeName.get(getTypes().getArrayType(getMirror(Object.class)))) + .isEqualTo(ArrayTypeName.of(ClassName.OBJECT)); + } + + @Test public void getVoidTypeMirror() { + assertThat(TypeName.get(getTypes().getNoType(TypeKind.VOID))) + .isEqualTo(TypeName.VOID); + } + + @Test public void getNullTypeMirror() { + try { + TypeName.get(getTypes().getNullType()); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void parameterizedType() throws Exception { + ParameterizedTypeName type = ParameterizedTypeName.get(Map.class, String.class, Long.class); + assertThat(type.toString()).isEqualTo("java.util.Map<java.lang.String, java.lang.Long>"); + } + + @Test public void arrayType() throws Exception { + ArrayTypeName type = ArrayTypeName.of(String.class); + assertThat(type.toString()).isEqualTo("java.lang.String[]"); + } + + @Test public void wildcardExtendsType() throws Exception { + WildcardTypeName type = WildcardTypeName.subtypeOf(CharSequence.class); + assertThat(type.toString()).isEqualTo("? extends java.lang.CharSequence"); + } + + @Test public void wildcardExtendsObject() throws Exception { + WildcardTypeName type = WildcardTypeName.subtypeOf(Object.class); + assertThat(type.toString()).isEqualTo("?"); + } + + @Test public void wildcardSuperType() throws Exception { + WildcardTypeName type = WildcardTypeName.supertypeOf(String.class); + assertThat(type.toString()).isEqualTo("? super java.lang.String"); + } + + @Test public void wildcardMirrorNoBounds() throws Exception { + WildcardType wildcard = getTypes().getWildcardType(null, null); + TypeName type = TypeName.get(wildcard); + assertThat(type.toString()).isEqualTo("?"); + } + + @Test public void wildcardMirrorExtendsType() throws Exception { + Types types = getTypes(); + Elements elements = getElements(); + TypeMirror charSequence = elements.getTypeElement(CharSequence.class.getName()).asType(); + WildcardType wildcard = types.getWildcardType(charSequence, null); + TypeName type = TypeName.get(wildcard); + assertThat(type.toString()).isEqualTo("? extends java.lang.CharSequence"); + } + + @Test public void wildcardMirrorSuperType() throws Exception { + Types types = getTypes(); + Elements elements = getElements(); + TypeMirror string = elements.getTypeElement(String.class.getName()).asType(); + WildcardType wildcard = types.getWildcardType(null, string); + TypeName type = TypeName.get(wildcard); + assertThat(type.toString()).isEqualTo("? super java.lang.String"); + } + + @Test public void typeVariable() throws Exception { + TypeVariableName type = TypeVariableName.get("T", CharSequence.class); + assertThat(type.toString()).isEqualTo("T"); // (Bounds are only emitted in declaration.) + } + + @Test public void box() throws Exception { + assertThat(TypeName.INT.box()).isEqualTo(ClassName.get(Integer.class)); + assertThat(TypeName.VOID.box()).isEqualTo(ClassName.get(Void.class)); + assertThat(ClassName.get(Integer.class).box()).isEqualTo(ClassName.get(Integer.class)); + assertThat(ClassName.get(Void.class).box()).isEqualTo(ClassName.get(Void.class)); + assertThat(TypeName.OBJECT.box()).isEqualTo(TypeName.OBJECT); + assertThat(ClassName.get(String.class).box()).isEqualTo(ClassName.get(String.class)); + } + + @Test public void unbox() throws Exception { + assertThat(TypeName.INT).isEqualTo(TypeName.INT.unbox()); + assertThat(TypeName.VOID).isEqualTo(TypeName.VOID.unbox()); + assertThat(ClassName.get(Integer.class).unbox()).isEqualTo(TypeName.INT.unbox()); + assertThat(ClassName.get(Void.class).unbox()).isEqualTo(TypeName.VOID.unbox()); + try { + TypeName.OBJECT.unbox(); + fail(); + } catch (UnsupportedOperationException expected) { + } + try { + ClassName.get(String.class).unbox(); + fail(); + } catch (UnsupportedOperationException expected) { + } + } +} diff --git a/src/test/java/com/squareup/javapoet/AnnotatedTypeNameTest.java b/src/test/java/com/squareup/javapoet/AnnotatedTypeNameTest.java new file mode 100644 index 0000000..42734ff --- /dev/null +++ b/src/test/java/com/squareup/javapoet/AnnotatedTypeNameTest.java @@ -0,0 +1,217 @@ +/* + * 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.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class AnnotatedTypeNameTest { + + private final static String NN = NeverNull.class.getCanonicalName(); + private final AnnotationSpec NEVER_NULL = AnnotationSpec.builder(NeverNull.class).build(); + private final static String TUA = TypeUseAnnotation.class.getCanonicalName(); + private final AnnotationSpec TYPE_USE_ANNOTATION = + AnnotationSpec.builder(TypeUseAnnotation.class).build(); + + @Target(ElementType.TYPE_USE) + public @interface NeverNull {} + + @Target(ElementType.TYPE_USE) + public @interface TypeUseAnnotation {} + + + @Test(expected=NullPointerException.class) public void nullAnnotationArray() { + TypeName.BOOLEAN.annotated((AnnotationSpec[]) null); + } + + @Test(expected=NullPointerException.class) public void nullAnnotationList() { + TypeName.DOUBLE.annotated((List<AnnotationSpec>) null); + } + + @Test public void annotated() { + TypeName simpleString = TypeName.get(String.class); + assertFalse(simpleString.isAnnotated()); + assertEquals(simpleString, TypeName.get(String.class)); + + TypeName annotated = simpleString.annotated(NEVER_NULL); + assertTrue(annotated.isAnnotated()); + assertEquals(annotated, annotated.annotated()); + } + + @Test public void annotatedType() { + TypeName type = TypeName.get(String.class); + TypeName actual = type.annotated(TYPE_USE_ANNOTATION); + assertThat(actual.toString()).isEqualTo("java.lang. @" + TUA + " String"); + } + + @Test public void annotatedTwice() { + TypeName type = TypeName.get(String.class); + TypeName actual = + type.annotated(NEVER_NULL) + .annotated(TYPE_USE_ANNOTATION); + assertThat(actual.toString()) + .isEqualTo("java.lang. @" + NN + " @" + TUA + " String"); + } + + @Test public void annotatedParameterizedType() { + TypeName type = ParameterizedTypeName.get(List.class, String.class); + TypeName actual = type.annotated(TYPE_USE_ANNOTATION); + assertThat(actual.toString()).isEqualTo("java.util. @" + TUA + " List<java.lang.String>"); + } + + @Test public void annotatedArgumentOfParameterizedType() { + TypeName type = TypeName.get(String.class).annotated(TYPE_USE_ANNOTATION); + TypeName actual = ParameterizedTypeName.get(ClassName.get(List.class), type); + assertThat(actual.toString()).isEqualTo("java.util.List<java.lang. @" + TUA + " String>"); + } + + @Test public void annotatedWildcardTypeNameWithSuper() { + TypeName type = TypeName.get(String.class).annotated(TYPE_USE_ANNOTATION); + TypeName actual = WildcardTypeName.supertypeOf(type); + assertThat(actual.toString()).isEqualTo("? super java.lang. @" + TUA + " String"); + } + + @Test public void annotatedWildcardTypeNameWithExtends() { + TypeName type = TypeName.get(String.class).annotated(TYPE_USE_ANNOTATION); + TypeName actual = WildcardTypeName.subtypeOf(type); + assertThat(actual.toString()).isEqualTo("? extends java.lang. @" + TUA + " String"); + } + + @Test public void annotatedEquivalence() { + annotatedEquivalence(TypeName.VOID); + annotatedEquivalence(ArrayTypeName.get(Object[].class)); + annotatedEquivalence(ClassName.get(Object.class)); + annotatedEquivalence(ParameterizedTypeName.get(List.class, Object.class)); + annotatedEquivalence(TypeVariableName.get(Object.class)); + annotatedEquivalence(WildcardTypeName.get(Object.class)); + } + + private void annotatedEquivalence(TypeName type) { + assertFalse(type.isAnnotated()); + assertEquals(type, type); + assertEquals(type.annotated(TYPE_USE_ANNOTATION), type.annotated(TYPE_USE_ANNOTATION)); + assertNotEquals(type, type.annotated(TYPE_USE_ANNOTATION)); + assertEquals(type.hashCode(), type.hashCode()); + assertEquals(type.annotated(TYPE_USE_ANNOTATION).hashCode(), + type.annotated(TYPE_USE_ANNOTATION).hashCode()); + assertNotEquals(type.hashCode(), type.annotated(TYPE_USE_ANNOTATION).hashCode()); + } + + // https://github.com/square/javapoet/issues/431 + @Test public void annotatedNestedType() { + TypeName type = TypeName.get(Map.Entry.class).annotated(TYPE_USE_ANNOTATION); + assertThat(type.toString()).isEqualTo("java.util.Map. @" + TUA + " Entry"); + } + + @Test public void annotatedEnclosingAndNestedType() { + TypeName type = ((ClassName) TypeName.get(Map.class).annotated(TYPE_USE_ANNOTATION)) + .nestedClass("Entry").annotated(TYPE_USE_ANNOTATION); + assertThat(type.toString()).isEqualTo("java.util. @" + TUA + " Map. @" + TUA + " Entry"); + } + + // https://github.com/square/javapoet/issues/431 + @Test public void annotatedNestedParameterizedType() { + TypeName type = ParameterizedTypeName.get(Map.Entry.class, Byte.class, Byte.class) + .annotated(TYPE_USE_ANNOTATION); + assertThat(type.toString()) + .isEqualTo("java.util.Map. @" + TUA + " Entry<java.lang.Byte, java.lang.Byte>"); + } + + @Test public void withoutAnnotationsOnAnnotatedEnclosingAndNestedType() { + TypeName type = ((ClassName) TypeName.get(Map.class).annotated(TYPE_USE_ANNOTATION)) + .nestedClass("Entry").annotated(TYPE_USE_ANNOTATION); + assertThat(type.isAnnotated()).isTrue(); + assertThat(type.withoutAnnotations()).isEqualTo(TypeName.get(Map.Entry.class)); + } + + @Test public void withoutAnnotationsOnAnnotatedEnclosingType() { + TypeName type = ((ClassName) TypeName.get(Map.class).annotated(TYPE_USE_ANNOTATION)) + .nestedClass("Entry"); + assertThat(type.isAnnotated()).isTrue(); + assertThat(type.withoutAnnotations()).isEqualTo(TypeName.get(Map.Entry.class)); + } + + @Test public void withoutAnnotationsOnAnnotatedNestedType() { + TypeName type = ((ClassName) TypeName.get(Map.class)) + .nestedClass("Entry").annotated(TYPE_USE_ANNOTATION); + assertThat(type.isAnnotated()).isTrue(); + assertThat(type.withoutAnnotations()).isEqualTo(TypeName.get(Map.Entry.class)); + } + + // https://github.com/square/javapoet/issues/614 + @Test public void annotatedArrayType() { + TypeName type = ArrayTypeName.of(ClassName.get(Object.class)).annotated(TYPE_USE_ANNOTATION); + assertThat(type.toString()).isEqualTo("java.lang.Object @" + TUA + " []"); + } + + @Test public void annotatedArrayElementType() { + TypeName type = ArrayTypeName.of(ClassName.get(Object.class).annotated(TYPE_USE_ANNOTATION)); + assertThat(type.toString()).isEqualTo("java.lang. @" + TUA + " Object[]"); + } + + // https://github.com/square/javapoet/issues/614 + @Test public void annotatedOuterMultidimensionalArrayType() { + TypeName type = ArrayTypeName.of(ArrayTypeName.of(ClassName.get(Object.class))) + .annotated(TYPE_USE_ANNOTATION); + assertThat(type.toString()).isEqualTo("java.lang.Object @" + TUA + " [][]"); + } + + // https://github.com/square/javapoet/issues/614 + @Test public void annotatedInnerMultidimensionalArrayType() { + TypeName type = ArrayTypeName.of(ArrayTypeName.of(ClassName.get(Object.class)) + .annotated(TYPE_USE_ANNOTATION)); + assertThat(type.toString()).isEqualTo("java.lang.Object[] @" + TUA + " []"); + } + + // https://github.com/square/javapoet/issues/614 + @Test public void annotatedArrayTypeVarargsParameter() { + TypeName type = ArrayTypeName.of(ArrayTypeName.of(ClassName.get(Object.class))) + .annotated(TYPE_USE_ANNOTATION); + MethodSpec varargsMethod = MethodSpec.methodBuilder("m") + .addParameter( + ParameterSpec.builder(type, "p") + .build()) + .varargs() + .build(); + assertThat(varargsMethod.toString()).isEqualTo("" + + "void m(java.lang.Object @" + TUA + " []... p) {\n" + + "}\n"); + } + + // https://github.com/square/javapoet/issues/614 + @Test public void annotatedArrayTypeInVarargsParameter() { + TypeName type = ArrayTypeName.of(ArrayTypeName.of(ClassName.get(Object.class)) + .annotated(TYPE_USE_ANNOTATION)); + MethodSpec varargsMethod = MethodSpec.methodBuilder("m") + .addParameter( + ParameterSpec.builder(type, "p") + .build()) + .varargs() + .build(); + assertThat(varargsMethod.toString()).isEqualTo("" + + "void m(java.lang.Object[] @" + TUA + " ... p) {\n" + + "}\n"); + } +} diff --git a/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java b/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java new file mode 100644 index 0000000..49606c7 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/AnnotationSpecTest.java @@ -0,0 +1,377 @@ +/* + * 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 com.google.testing.compile.CompilationRule; +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.lang.model.element.TypeElement; +import org.junit.Rule; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +public final class AnnotationSpecTest { + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationA { + } + + @Inherited + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationB { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationC { + String value(); + } + + public enum Breakfast { + WAFFLES, PANCAKES; + public String toString() { return name() + " with cherries!"; }; + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface HasDefaultsAnnotation { + + byte a() default 5; + + short b() default 6; + + int c() default 7; + + long d() default 8; + + float e() default 9.0f; + + double f() default 10.0; + + char[] g() default {0, 0xCAFE, 'z', '€', 'ℕ', '"', '\'', '\t', '\n'}; + + boolean h() default true; + + Breakfast i() default Breakfast.WAFFLES; + + AnnotationA j() default @AnnotationA(); + + String k() default "maple"; + + Class<? extends Annotation> l() default AnnotationB.class; + + int[] m() default {1, 2, 3}; + + Breakfast[] n() default {Breakfast.WAFFLES, Breakfast.PANCAKES}; + + Breakfast o(); + + int p(); + + AnnotationC q() default @AnnotationC("foo"); + + Class<? extends Number>[] r() default {Byte.class, Short.class, Integer.class, Long.class}; + + } + + @HasDefaultsAnnotation( + o = Breakfast.PANCAKES, + p = 1701, + f = 11.1, + m = {9, 8, 1}, + l = Override.class, + j = @AnnotationA, + q = @AnnotationC("bar"), + r = {Float.class, Double.class}) + public class IsAnnotated { + // empty + } + + @Rule public final CompilationRule compilation = new CompilationRule(); + + @Test public void equalsAndHashCode() { + AnnotationSpec a = AnnotationSpec.builder(AnnotationC.class).build(); + AnnotationSpec b = AnnotationSpec.builder(AnnotationC.class).build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = AnnotationSpec.builder(AnnotationC.class).addMember("value", "$S", "123").build(); + b = AnnotationSpec.builder(AnnotationC.class).addMember("value", "$S", "123").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test public void defaultAnnotation() { + String name = IsAnnotated.class.getCanonicalName(); + TypeElement element = compilation.getElements().getTypeElement(name); + AnnotationSpec annotation = AnnotationSpec.get(element.getAnnotationMirrors().get(0)); + + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addAnnotation(annotation) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.javapoet.AnnotationSpecTest;\n" + + "import java.lang.Double;\n" + + "import java.lang.Float;\n" + + "import java.lang.Override;\n" + + "\n" + + "@AnnotationSpecTest.HasDefaultsAnnotation(\n" + + " o = AnnotationSpecTest.Breakfast.PANCAKES,\n" + + " p = 1701,\n" + + " f = 11.1,\n" + + " m = {\n" + + " 9,\n" + + " 8,\n" + + " 1\n" + + " },\n" + + " l = Override.class,\n" + + " j = @AnnotationSpecTest.AnnotationA,\n" + + " q = @AnnotationSpecTest.AnnotationC(\"bar\"),\n" + + " r = {\n" + + " Float.class,\n" + + " Double.class\n" + + " }\n" + + ")\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void defaultAnnotationWithImport() { + String name = IsAnnotated.class.getCanonicalName(); + TypeElement element = compilation.getElements().getTypeElement(name); + AnnotationSpec annotation = AnnotationSpec.get(element.getAnnotationMirrors().get(0)); + TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(IsAnnotated.class.getSimpleName()); + typeBuilder.addAnnotation(annotation); + JavaFile file = JavaFile.builder("com.squareup.javapoet", typeBuilder.build()).build(); + assertThat(file.toString()).isEqualTo( + "package com.squareup.javapoet;\n" + + "\n" + + "import java.lang.Double;\n" + + "import java.lang.Float;\n" + + "import java.lang.Override;\n" + + "\n" + + "@AnnotationSpecTest.HasDefaultsAnnotation(\n" + + " o = AnnotationSpecTest.Breakfast.PANCAKES,\n" + + " p = 1701,\n" + + " f = 11.1,\n" + + " m = {\n" + + " 9,\n" + + " 8,\n" + + " 1\n" + + " },\n" + + " l = Override.class,\n" + + " j = @AnnotationSpecTest.AnnotationA,\n" + + " q = @AnnotationSpecTest.AnnotationC(\"bar\"),\n" + + " r = {\n" + + " Float.class,\n" + + " Double.class\n" + + " }\n" + + ")\n" + + "class IsAnnotated {\n" + + "}\n" + ); + } + + @Test public void emptyArray() { + AnnotationSpec.Builder builder = AnnotationSpec.builder(HasDefaultsAnnotation.class); + builder.addMember("n", "$L", "{}"); + assertThat(builder.build().toString()).isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + "n = {}" + ")"); + builder.addMember("m", "$L", "{}"); + assertThat(builder.build().toString()) + .isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = {}, m = {}" + + ")"); + } + + @Test public void dynamicArrayOfEnumConstants() { + AnnotationSpec.Builder builder = AnnotationSpec.builder(HasDefaultsAnnotation.class); + builder.addMember("n", "$T.$L", Breakfast.class, Breakfast.PANCAKES.name()); + assertThat(builder.build().toString()).isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + ")"); + + // builder = AnnotationSpec.builder(HasDefaultsAnnotation.class); + builder.addMember("n", "$T.$L", Breakfast.class, Breakfast.WAFFLES.name()); + builder.addMember("n", "$T.$L", Breakfast.class, Breakfast.PANCAKES.name()); + assertThat(builder.build().toString()).isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = {" + + "com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.WAFFLES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + "})"); + + builder = builder.build().toBuilder(); // idempotent + assertThat(builder.build().toString()).isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = {" + + "com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.WAFFLES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + "})"); + + builder.addMember("n", "$T.$L", Breakfast.class, Breakfast.WAFFLES.name()); + assertThat(builder.build().toString()).isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = {" + + "com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.WAFFLES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + ", com.squareup.javapoet.AnnotationSpecTest.Breakfast.WAFFLES" + + "})"); + } + + @Test public void defaultAnnotationToBuilder() { + String name = IsAnnotated.class.getCanonicalName(); + TypeElement element = compilation.getElements().getTypeElement(name); + AnnotationSpec.Builder builder = AnnotationSpec.get(element.getAnnotationMirrors().get(0)) + .toBuilder(); + builder.addMember("m", "$L", 123); + assertThat(builder.build().toString()).isEqualTo( + "@com.squareup.javapoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "o = com.squareup.javapoet.AnnotationSpecTest.Breakfast.PANCAKES" + + ", p = 1701" + + ", f = 11.1" + + ", m = {9, 8, 1, 123}" + + ", l = java.lang.Override.class" + + ", j = @com.squareup.javapoet.AnnotationSpecTest.AnnotationA" + + ", q = @com.squareup.javapoet.AnnotationSpecTest.AnnotationC(\"bar\")" + + ", r = {java.lang.Float.class, java.lang.Double.class}" + + ")"); + } + + @Test public void reflectAnnotation() { + HasDefaultsAnnotation annotation = IsAnnotated.class.getAnnotation(HasDefaultsAnnotation.class); + AnnotationSpec spec = AnnotationSpec.get(annotation); + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addAnnotation(spec) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.javapoet.AnnotationSpecTest;\n" + + "import java.lang.Double;\n" + + "import java.lang.Float;\n" + + "import java.lang.Override;\n" + + "\n" + + "@AnnotationSpecTest.HasDefaultsAnnotation(\n" + + " f = 11.1,\n" + + " l = Override.class,\n" + + " m = {\n" + + " 9,\n" + + " 8,\n" + + " 1\n" + + " },\n" + + " o = AnnotationSpecTest.Breakfast.PANCAKES,\n" + + " p = 1701,\n" + + " q = @AnnotationSpecTest.AnnotationC(\"bar\"),\n" + + " r = {\n" + + " Float.class,\n" + + " Double.class\n" + + " }\n" + + ")\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void reflectAnnotationWithDefaults() { + HasDefaultsAnnotation annotation = IsAnnotated.class.getAnnotation(HasDefaultsAnnotation.class); + AnnotationSpec spec = AnnotationSpec.get(annotation, true); + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addAnnotation(spec) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.javapoet.AnnotationSpecTest;\n" + + "import java.lang.Double;\n" + + "import java.lang.Float;\n" + + "import java.lang.Override;\n" + + "\n" + + "@AnnotationSpecTest.HasDefaultsAnnotation(\n" + + " a = 5,\n" + + " b = 6,\n" + + " c = 7,\n" + + " d = 8,\n" + + " e = 9.0f,\n" + + " f = 11.1,\n" + + " g = {\n" + + " '\\u0000',\n" + + " '쫾',\n" + + " 'z',\n" + + " '€',\n" + + " 'ℕ',\n" + + " '\"',\n" + + " '\\'',\n" + + " '\\t',\n" + + " '\\n'\n" + + " },\n" + + " h = true,\n" + + " i = AnnotationSpecTest.Breakfast.WAFFLES,\n" + + " j = @AnnotationSpecTest.AnnotationA,\n" + + " k = \"maple\",\n" + + " l = Override.class,\n" + + " m = {\n" + + " 9,\n" + + " 8,\n" + + " 1\n" + + " },\n" + + " n = {\n" + + " AnnotationSpecTest.Breakfast.WAFFLES,\n" + + " AnnotationSpecTest.Breakfast.PANCAKES\n" + + " },\n" + + " o = AnnotationSpecTest.Breakfast.PANCAKES,\n" + + " p = 1701,\n" + + " q = @AnnotationSpecTest.AnnotationC(\"bar\"),\n" + + " r = {\n" + + " Float.class,\n" + + " Double.class\n" + + " }\n" + + ")\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void disallowsNullMemberName() { + AnnotationSpec.Builder builder = AnnotationSpec.builder(HasDefaultsAnnotation.class); + try { + AnnotationSpec.Builder $L = builder.addMember(null, "$L", ""); + fail($L.build().toString()); + } catch (NullPointerException e) { + assertThat(e).hasMessageThat().isEqualTo("name == null"); + } + } + + @Test public void requiresValidMemberName() { + AnnotationSpec.Builder builder = AnnotationSpec.builder(HasDefaultsAnnotation.class); + try { + AnnotationSpec.Builder $L = builder.addMember("@", "$L", ""); + fail($L.build().toString()); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("not a valid name: @"); + } + } + + private String toString(TypeSpec typeSpec) { + return JavaFile.builder("com.squareup.tacos", typeSpec).build().toString(); + } +} diff --git a/src/test/java/com/squareup/javapoet/ClassNameTest.java b/src/test/java/com/squareup/javapoet/ClassNameTest.java new file mode 100644 index 0000000..e2cc55e --- /dev/null +++ b/src/test/java/com/squareup/javapoet/ClassNameTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2014 Google, 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 com.google.testing.compile.CompilationRule; +import java.util.Map; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +@RunWith(JUnit4.class) +public final class ClassNameTest { + @Rule public CompilationRule compilationRule = new CompilationRule(); + + @Test public void bestGuessForString_simpleClass() { + assertThat(ClassName.bestGuess(String.class.getName())) + .isEqualTo(ClassName.get("java.lang", "String")); + } + + @Test public void bestGuessNonAscii() { + ClassName className = ClassName.bestGuess( + "com.\ud835\udc1andro\ud835\udc22d.\ud835\udc00ctiv\ud835\udc22ty"); + assertEquals("com.\ud835\udc1andro\ud835\udc22d", className.packageName()); + assertEquals("\ud835\udc00ctiv\ud835\udc22ty", className.simpleName()); + } + + static class OuterClass { + static class InnerClass {} + } + + @Test public void bestGuessForString_nestedClass() { + assertThat(ClassName.bestGuess(Map.Entry.class.getCanonicalName())) + .isEqualTo(ClassName.get("java.util", "Map", "Entry")); + assertThat(ClassName.bestGuess(OuterClass.InnerClass.class.getCanonicalName())) + .isEqualTo(ClassName.get("com.squareup.javapoet", + "ClassNameTest", "OuterClass", "InnerClass")); + } + + @Test public void bestGuessForString_defaultPackage() { + assertThat(ClassName.bestGuess("SomeClass")) + .isEqualTo(ClassName.get("", "SomeClass")); + assertThat(ClassName.bestGuess("SomeClass.Nested")) + .isEqualTo(ClassName.get("", "SomeClass", "Nested")); + assertThat(ClassName.bestGuess("SomeClass.Nested.EvenMore")) + .isEqualTo(ClassName.get("", "SomeClass", "Nested", "EvenMore")); + } + + @Test public void bestGuessForString_confusingInput() { + assertBestGuessThrows(""); + assertBestGuessThrows("."); + assertBestGuessThrows(".Map"); + assertBestGuessThrows("java"); + assertBestGuessThrows("java.util"); + assertBestGuessThrows("java.util."); + assertBestGuessThrows("java..util.Map.Entry"); + assertBestGuessThrows("java.util..Map.Entry"); + assertBestGuessThrows("java.util.Map..Entry"); + assertBestGuessThrows("com.test.$"); + assertBestGuessThrows("com.test.LooksLikeAClass.pkg"); + assertBestGuessThrows("!@#$gibberish%^&*"); + } + + private void assertBestGuessThrows(String s) { + try { + ClassName.bestGuess(s); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void createNestedClass() { + ClassName foo = ClassName.get("com.example", "Foo"); + ClassName bar = foo.nestedClass("Bar"); + assertThat(bar).isEqualTo(ClassName.get("com.example", "Foo", "Bar")); + ClassName baz = bar.nestedClass("Baz"); + assertThat(baz).isEqualTo(ClassName.get("com.example", "Foo", "Bar", "Baz")); + } + + static class $Outer { + static class $Inner {} + } + + @Test public void classNameFromTypeElement() { + Elements elements = compilationRule.getElements(); + TypeElement object = elements.getTypeElement(Object.class.getCanonicalName()); + assertThat(ClassName.get(object).toString()).isEqualTo("java.lang.Object"); + TypeElement outer = elements.getTypeElement($Outer.class.getCanonicalName()); + assertThat(ClassName.get(outer).toString()).isEqualTo("com.squareup.javapoet.ClassNameTest.$Outer"); + TypeElement inner = elements.getTypeElement($Outer.$Inner.class.getCanonicalName()); + assertThat(ClassName.get(inner).toString()).isEqualTo("com.squareup.javapoet.ClassNameTest.$Outer.$Inner"); + } + + /** + * Buck builds with "source-based ABI generation" and those builds don't support + * {@link TypeElement#getKind()}. Test to confirm that we don't use that API. + */ + @Test public void classNameFromTypeElementDoesntUseGetKind() { + Elements elements = compilationRule.getElements(); + TypeElement object = elements.getTypeElement(Object.class.getCanonicalName()); + assertThat(ClassName.get(preventGetKind(object)).toString()) + .isEqualTo("java.lang.Object"); + TypeElement outer = elements.getTypeElement($Outer.class.getCanonicalName()); + assertThat(ClassName.get(preventGetKind(outer)).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest.$Outer"); + TypeElement inner = elements.getTypeElement($Outer.$Inner.class.getCanonicalName()); + assertThat(ClassName.get(preventGetKind(inner)).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest.$Outer.$Inner"); + } + + /** Returns a new instance like {@code object} that throws on {@code getKind()}. */ + private TypeElement preventGetKind(TypeElement object) { + TypeElement spy = Mockito.spy(object); + when(spy.getKind()).thenThrow(new AssertionError()); + when(spy.getEnclosingElement()).thenAnswer(invocation -> { + Object enclosingElement = invocation.callRealMethod(); + return enclosingElement instanceof TypeElement + ? preventGetKind((TypeElement) enclosingElement) + : enclosingElement; + }); + return spy; + } + + @Test public void classNameFromClass() { + assertThat(ClassName.get(Object.class).toString()) + .isEqualTo("java.lang.Object"); + assertThat(ClassName.get(OuterClass.InnerClass.class).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest.OuterClass.InnerClass"); + assertThat((ClassName.get(new Object() {}.getClass())).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest$1"); + assertThat((ClassName.get(new Object() { Object inner = new Object() {}; }.inner.getClass())).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest$2$1"); + assertThat((ClassName.get($Outer.class)).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest.$Outer"); + assertThat((ClassName.get($Outer.$Inner.class)).toString()) + .isEqualTo("com.squareup.javapoet.ClassNameTest.$Outer.$Inner"); + } + + @Test public void peerClass() { + assertThat(ClassName.get(Double.class).peerClass("Short")) + .isEqualTo(ClassName.get(Short.class)); + assertThat(ClassName.get("", "Double").peerClass("Short")) + .isEqualTo(ClassName.get("", "Short")); + assertThat(ClassName.get("a.b", "Combo", "Taco").peerClass("Burrito")) + .isEqualTo(ClassName.get("a.b", "Combo", "Burrito")); + } + + @Test public void fromClassRejectionTypes() { + try { + ClassName.get(int.class); + fail(); + } catch (IllegalArgumentException ignored) { + } + try { + ClassName.get(void.class); + fail(); + } catch (IllegalArgumentException ignored) { + } + try { + ClassName.get(Object[].class); + fail(); + } catch (IllegalArgumentException ignored) { + } + } + + @Test + public void reflectionName() { + assertEquals("java.lang.Object", TypeName.OBJECT.reflectionName()); + assertEquals("java.lang.Thread$State", ClassName.get(Thread.State.class).reflectionName()); + assertEquals("java.util.Map$Entry", ClassName.get(Map.Entry.class).reflectionName()); + assertEquals("Foo", ClassName.get("", "Foo").reflectionName()); + assertEquals("Foo$Bar$Baz", ClassName.get("", "Foo", "Bar", "Baz").reflectionName()); + assertEquals("a.b.c.Foo$Bar$Baz", ClassName.get("a.b.c", "Foo", "Bar", "Baz").reflectionName()); + } +} diff --git a/src/test/java/com/squareup/javapoet/CodeBlockTest.java b/src/test/java/com/squareup/javapoet/CodeBlockTest.java new file mode 100644 index 0000000..2862809 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/CodeBlockTest.java @@ -0,0 +1,342 @@ +/* + * 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.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public final class CodeBlockTest { + @Test public void equalsAndHashCode() { + CodeBlock a = CodeBlock.builder().build(); + CodeBlock b = CodeBlock.builder().build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = CodeBlock.builder().add("$L", "taco").build(); + b = CodeBlock.builder().add("$L", "taco").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test public void of() { + CodeBlock a = CodeBlock.of("$L taco", "delicious"); + assertThat(a.toString()).isEqualTo("delicious taco"); + } + + @Test public void isEmpty() { + assertTrue(CodeBlock.builder().isEmpty()); + assertTrue(CodeBlock.builder().add("").isEmpty()); + assertFalse(CodeBlock.builder().add(" ").isEmpty()); + } + + @Test public void indentCannotBeIndexed() { + try { + CodeBlock.builder().add("$1>", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test public void deindentCannotBeIndexed() { + try { + CodeBlock.builder().add("$1<", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test public void dollarSignEscapeCannotBeIndexed() { + try { + CodeBlock.builder().add("$1$", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test public void statementBeginningCannotBeIndexed() { + try { + CodeBlock.builder().add("$1[", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test public void statementEndingCannotBeIndexed() { + try { + CodeBlock.builder().add("$1]", "taco").build(); + fail(); + } catch (IllegalArgumentException exp) { + assertThat(exp) + .hasMessageThat() + .isEqualTo("$$, $>, $<, $[, $], $W, and $Z may not have an index"); + } + } + + @Test public void nameFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1N", "taco").build(); + assertThat(block.toString()).isEqualTo("taco"); + } + + @Test public void literalFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1L", "taco").build(); + assertThat(block.toString()).isEqualTo("taco"); + } + + @Test public void stringFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1S", "taco").build(); + assertThat(block.toString()).isEqualTo("\"taco\""); + } + + @Test public void typeFormatCanBeIndexed() { + CodeBlock block = CodeBlock.builder().add("$1T", String.class).build(); + assertThat(block.toString()).isEqualTo("java.lang.String"); + } + + @Test public void simpleNamedArgument() { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("text", "taco"); + CodeBlock block = CodeBlock.builder().addNamed("$text:S", map).build(); + assertThat(block.toString()).isEqualTo("\"taco\""); + } + + @Test public void repeatedNamedArgument() { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("text", "tacos"); + CodeBlock block = CodeBlock.builder() + .addNamed("\"I like \" + $text:S + \". Do you like \" + $text:S + \"?\"", map) + .build(); + assertThat(block.toString()).isEqualTo( + "\"I like \" + \"tacos\" + \". Do you like \" + \"tacos\" + \"?\""); + } + + @Test public void namedAndNoArgFormat() { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("text", "tacos"); + CodeBlock block = CodeBlock.builder() + .addNamed("$>\n$text:L for $$3.50", map).build(); + assertThat(block.toString()).isEqualTo("\n tacos for $3.50"); + } + + @Test public void missingNamedArgument() { + try { + Map<String, Object> map = new LinkedHashMap<>(); + CodeBlock.builder().addNamed("$text:S", map).build(); + fail(); + } catch(IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Missing named argument for $text"); + } + } + + @Test public void lowerCaseNamed() { + try { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("Text", "tacos"); + CodeBlock block = CodeBlock.builder().addNamed("$Text:S", map).build(); + fail(); + } catch(IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("argument 'Text' must start with a lowercase character"); + } + } + + @Test public void multipleNamedArguments() { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("pipe", System.class); + map.put("text", "tacos"); + + CodeBlock block = CodeBlock.builder() + .addNamed("$pipe:T.out.println(\"Let's eat some $text:L\");", map) + .build(); + + assertThat(block.toString()).isEqualTo( + "java.lang.System.out.println(\"Let's eat some tacos\");"); + } + + @Test public void namedNewline() { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("clazz", Integer.class); + CodeBlock block = CodeBlock.builder().addNamed("$clazz:T\n", map).build(); + assertThat(block.toString()).isEqualTo("java.lang.Integer\n"); + } + + @Test public void danglingNamed() { + Map<String, Object> map = new LinkedHashMap<>(); + map.put("clazz", Integer.class); + try { + CodeBlock.builder().addNamed("$clazz:T$", map).build(); + fail(); + } catch(IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("dangling $ at end"); + } + } + + @Test public void indexTooHigh() { + try { + CodeBlock.builder().add("$2T", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 2 for '$2T' not in range (received 1 arguments)"); + } + } + + @Test public void indexIsZero() { + try { + CodeBlock.builder().add("$0T", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 0 for '$0T' not in range (received 1 arguments)"); + } + } + + @Test public void indexIsNegative() { + try { + CodeBlock.builder().add("$-1T", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$-1T'"); + } + } + + @Test public void indexWithoutFormatType() { + try { + CodeBlock.builder().add("$1", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("dangling format characters in '$1'"); + } + } + + @Test public void indexWithoutFormatTypeNotAtStringEnd() { + try { + CodeBlock.builder().add("$1 taco", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$1 taco'"); + } + } + + @Test public void indexButNoArguments() { + try { + CodeBlock.builder().add("$1T").build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 1 for '$1T' not in range (received 0 arguments)"); + } + } + + @Test public void formatIndicatorAlone() { + try { + CodeBlock.builder().add("$", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("dangling format characters in '$'"); + } + } + + @Test public void formatIndicatorWithoutIndexOrFormatType() { + try { + CodeBlock.builder().add("$ tacoString", String.class).build(); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("invalid format string: '$ tacoString'"); + } + } + + @Test public void sameIndexCanBeUsedWithDifferentFormats() { + CodeBlock block = CodeBlock.builder() + .add("$1T.out.println($1S)", ClassName.get(System.class)) + .build(); + assertThat(block.toString()).isEqualTo("java.lang.System.out.println(\"java.lang.System\")"); + } + + @Test public void tooManyStatementEnters() { + CodeBlock codeBlock = CodeBlock.builder().add("$[$[").build(); + try { + // We can't report this error until rendering type because code blocks might be composed. + codeBlock.toString(); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("statement enter $[ followed by statement enter $["); + } + } + + @Test public void statementExitWithoutStatementEnter() { + CodeBlock codeBlock = CodeBlock.builder().add("$]").build(); + try { + // We can't report this error until rendering type because code blocks might be composed. + codeBlock.toString(); + fail(); + } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("statement exit $] has no matching statement enter $["); + } + } + + @Test public void join() { + List<CodeBlock> codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); + codeBlocks.add(CodeBlock.of("need tacos")); + + CodeBlock joined = CodeBlock.join(codeBlocks, " || "); + assertThat(joined.toString()).isEqualTo("\"hello\" || world.World || need tacos"); + } + + @Test public void joining() { + List<CodeBlock> codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); + codeBlocks.add(CodeBlock.of("need tacos")); + + CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ")); + assertThat(joined.toString()).isEqualTo("\"hello\" || world.World || need tacos"); + } + + @Test public void joiningSingle() { + List<CodeBlock> codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + + CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ")); + assertThat(joined.toString()).isEqualTo("\"hello\""); + } + + @Test public void joiningWithPrefixAndSuffix() { + List<CodeBlock> codeBlocks = new ArrayList<>(); + codeBlocks.add(CodeBlock.of("$S", "hello")); + codeBlocks.add(CodeBlock.of("$T", ClassName.get("world", "World"))); + codeBlocks.add(CodeBlock.of("need tacos")); + + CodeBlock joined = codeBlocks.stream().collect(CodeBlock.joining(" || ", "start {", "} end")); + assertThat(joined.toString()).isEqualTo("start {\"hello\" || world.World || need tacos} end"); + } +} diff --git a/src/test/java/com/squareup/javapoet/FieldSpecTest.java b/src/test/java/com/squareup/javapoet/FieldSpecTest.java new file mode 100644 index 0000000..63f7aa8 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/FieldSpecTest.java @@ -0,0 +1,47 @@ +/* + * 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 org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import javax.lang.model.element.Modifier; + +public class FieldSpecTest { + @Test public void equalsAndHashCode() { + FieldSpec a = FieldSpec.builder(int.class, "foo").build(); + FieldSpec b = FieldSpec.builder(int.class, "foo").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = FieldSpec.builder(int.class, "FOO", Modifier.PUBLIC, Modifier.STATIC).build(); + b = FieldSpec.builder(int.class, "FOO", Modifier.PUBLIC, Modifier.STATIC).build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test public void nullAnnotationsAddition() { + try { + FieldSpec.builder(int.class, "foo").addAnnotations(null); + fail(); + } + catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("annotationSpecs == null"); + } + } +}
\ No newline at end of file diff --git a/src/test/java/com/squareup/javapoet/FileReadingTest.java b/src/test/java/com/squareup/javapoet/FileReadingTest.java new file mode 100644 index 0000000..eb19de0 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/FileReadingTest.java @@ -0,0 +1,122 @@ +/* + * 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 com.google.common.io.ByteStreams; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.Locale; +import java.util.concurrent.Callable; +import javax.lang.model.element.Modifier; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +@RunWith(JUnit4.class) +public class FileReadingTest { + + // Used for storing compilation output. + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test public void javaFileObjectUri() { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + assertThat(JavaFile.builder("", type).build().toJavaFileObject().toUri()) + .isEqualTo(URI.create("Test.java")); + assertThat(JavaFile.builder("foo", type).build().toJavaFileObject().toUri()) + .isEqualTo(URI.create("foo/Test.java")); + assertThat(JavaFile.builder("com.example", type).build().toJavaFileObject().toUri()) + .isEqualTo(URI.create("com/example/Test.java")); + } + + @Test public void javaFileObjectKind() { + JavaFile javaFile = JavaFile.builder("", TypeSpec.classBuilder("Test").build()).build(); + assertThat(javaFile.toJavaFileObject().getKind()).isEqualTo(Kind.SOURCE); + } + + @Test public void javaFileObjectCharacterContent() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test") + .addJavadoc("Pi\u00f1ata\u00a1") + .addMethod(MethodSpec.methodBuilder("fooBar").build()) + .build(); + JavaFile javaFile = JavaFile.builder("foo", type).build(); + JavaFileObject javaFileObject = javaFile.toJavaFileObject(); + + // We can never have encoding issues (everything is in process) + assertThat(javaFileObject.getCharContent(true)).isEqualTo(javaFile.toString()); + assertThat(javaFileObject.getCharContent(false)).isEqualTo(javaFile.toString()); + } + + @Test public void javaFileObjectInputStreamIsUtf8() throws IOException { + JavaFile javaFile = JavaFile.builder("foo", TypeSpec.classBuilder("Test").build()) + .addFileComment("Pi\u00f1ata\u00a1") + .build(); + byte[] bytes = ByteStreams.toByteArray(javaFile.toJavaFileObject().openInputStream()); + + // JavaPoet always uses UTF-8. + assertThat(bytes).isEqualTo(javaFile.toString().getBytes(UTF_8)); + } + + @Test public void compileJavaFile() throws Exception { + final String value = "Hello World!"; + TypeSpec type = TypeSpec.classBuilder("Test") + .addModifiers(Modifier.PUBLIC) + .addSuperinterface(ParameterizedTypeName.get(Callable.class, String.class)) + .addMethod(MethodSpec.methodBuilder("call") + .returns(String.class) + .addModifiers(Modifier.PUBLIC) + .addStatement("return $S", value) + .build()) + .build(); + JavaFile javaFile = JavaFile.builder("foo", type).build(); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticCollector, + Locale.getDefault(), UTF_8); + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, + Collections.singleton(temporaryFolder.newFolder())); + CompilationTask task = compiler.getTask(null, + fileManager, + diagnosticCollector, + Collections.emptySet(), + Collections.emptySet(), + Collections.singleton(javaFile.toJavaFileObject())); + + assertThat(task.call()).isTrue(); + assertThat(diagnosticCollector.getDiagnostics()).isEmpty(); + + ClassLoader loader = fileManager.getClassLoader(StandardLocation.CLASS_OUTPUT); + Callable<?> test = Class.forName("foo.Test", true, loader) + .asSubclass(Callable.class) + .getDeclaredConstructor() + .newInstance(); + assertThat(Callable.class.getMethod("call").invoke(test)).isEqualTo(value); + } +} diff --git a/src/test/java/com/squareup/javapoet/FileWritingTest.java b/src/test/java/com/squareup/javapoet/FileWritingTest.java new file mode 100644 index 0000000..f817ddb --- /dev/null +++ b/src/test/java/com/squareup/javapoet/FileWritingTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2014 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 com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public final class FileWritingTest { + // Used for testing java.io File behavior. + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + // Used for testing java.nio.file Path behavior. + private final FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); + private final Path fsRoot = fs.getRootDirectories().iterator().next(); + + // Used for testing annotation processor Filer behavior. + private final TestFiler filer = new TestFiler(fs, fsRoot); + + @Test public void pathNotDirectory() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile javaFile = JavaFile.builder("example", type).build(); + Path path = fs.getPath("/foo/bar"); + Files.createDirectories(path.getParent()); + Files.createFile(path); + try { + javaFile.writeTo(path); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo("path /foo/bar exists but is not a directory."); + } + } + + @Test public void fileNotDirectory() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile javaFile = JavaFile.builder("example", type).build(); + File file = new File(tmp.newFolder("foo"), "bar"); + file.createNewFile(); + try { + javaFile.writeTo(file); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo( + "path " + file.getPath() + " exists but is not a directory."); + } + } + + @Test public void pathDefaultPackage() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile.builder("", type).build().writeTo(fsRoot); + + Path testPath = fsRoot.resolve("Test.java"); + assertThat(Files.exists(testPath)).isTrue(); + } + + @Test public void fileDefaultPackage() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile.builder("", type).build().writeTo(tmp.getRoot()); + + File testFile = new File(tmp.getRoot(), "Test.java"); + assertThat(testFile.exists()).isTrue(); + } + + @Test public void filerDefaultPackage() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile.builder("", type).build().writeTo(filer); + + Path testPath = fsRoot.resolve("Test.java"); + assertThat(Files.exists(testPath)).isTrue(); + } + + @Test public void pathNestedClasses() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile.builder("foo", type).build().writeTo(fsRoot); + JavaFile.builder("foo.bar", type).build().writeTo(fsRoot); + JavaFile.builder("foo.bar.baz", type).build().writeTo(fsRoot); + + Path fooPath = fsRoot.resolve(fs.getPath("foo", "Test.java")); + Path barPath = fsRoot.resolve(fs.getPath("foo", "bar", "Test.java")); + Path bazPath = fsRoot.resolve(fs.getPath("foo", "bar", "baz", "Test.java")); + assertThat(Files.exists(fooPath)).isTrue(); + assertThat(Files.exists(barPath)).isTrue(); + assertThat(Files.exists(bazPath)).isTrue(); + } + + @Test public void fileNestedClasses() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile.builder("foo", type).build().writeTo(tmp.getRoot()); + JavaFile.builder("foo.bar", type).build().writeTo(tmp.getRoot()); + JavaFile.builder("foo.bar.baz", type).build().writeTo(tmp.getRoot()); + + File fooDir = new File(tmp.getRoot(), "foo"); + File fooFile = new File(fooDir, "Test.java"); + File barDir = new File(fooDir, "bar"); + File barFile = new File(barDir, "Test.java"); + File bazDir = new File(barDir, "baz"); + File bazFile = new File(bazDir, "Test.java"); + assertThat(fooFile.exists()).isTrue(); + assertThat(barFile.exists()).isTrue(); + assertThat(bazFile.exists()).isTrue(); + } + + @Test public void filerNestedClasses() throws IOException { + TypeSpec type = TypeSpec.classBuilder("Test").build(); + JavaFile.builder("foo", type).build().writeTo(filer); + JavaFile.builder("foo.bar", type).build().writeTo(filer); + JavaFile.builder("foo.bar.baz", type).build().writeTo(filer); + + Path fooPath = fsRoot.resolve(fs.getPath("foo", "Test.java")); + Path barPath = fsRoot.resolve(fs.getPath("foo", "bar", "Test.java")); + Path bazPath = fsRoot.resolve(fs.getPath("foo", "bar", "baz", "Test.java")); + assertThat(Files.exists(fooPath)).isTrue(); + assertThat(Files.exists(barPath)).isTrue(); + assertThat(Files.exists(bazPath)).isTrue(); + } + + @Test public void filerPassesOriginatingElements() throws IOException { + Element element1_1 = Mockito.mock(Element.class); + TypeSpec test1 = TypeSpec.classBuilder("Test1") + .addOriginatingElement(element1_1) + .build(); + + Element element2_1 = Mockito.mock(Element.class); + Element element2_2 = Mockito.mock(Element.class); + TypeSpec test2 = TypeSpec.classBuilder("Test2") + .addOriginatingElement(element2_1) + .addOriginatingElement(element2_2) + .build(); + + JavaFile.builder("example", test1).build().writeTo(filer); + JavaFile.builder("example", test2).build().writeTo(filer); + + Path testPath1 = fsRoot.resolve(fs.getPath("example", "Test1.java")); + assertThat(filer.getOriginatingElements(testPath1)).containsExactly(element1_1); + Path testPath2 = fsRoot.resolve(fs.getPath("example", "Test2.java")); + assertThat(filer.getOriginatingElements(testPath2)).containsExactly(element2_1, element2_2); + } + + @Test public void filerClassesWithTabIndent() throws IOException { + TypeSpec test = TypeSpec.classBuilder("Test") + .addField(Date.class, "madeFreshDate") + .addMethod(MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(String[].class, "args") + .addCode("$T.out.println($S);\n", System.class, "Hello World!") + .build()) + .build(); + JavaFile.builder("foo", test).indent("\t").build().writeTo(filer); + + Path fooPath = fsRoot.resolve(fs.getPath("foo", "Test.java")); + assertThat(Files.exists(fooPath)).isTrue(); + String source = new String(Files.readAllBytes(fooPath)); + + assertThat(source).isEqualTo("" + + "package foo;\n" + + "\n" + + "import java.lang.String;\n" + + "import java.lang.System;\n" + + "import java.util.Date;\n" + + "\n" + + "class Test {\n" + + "\tDate madeFreshDate;\n" + + "\n" + + "\tpublic static void main(String[] args) {\n" + + "\t\tSystem.out.println(\"Hello World!\");\n" + + "\t}\n" + + "}\n"); + } + + /** + * This test confirms that JavaPoet ignores the host charset and always uses UTF-8. The host + * charset is customized with {@code -Dfile.encoding=ISO-8859-1}. + */ + @Test public void fileIsUtf8() throws IOException { + JavaFile javaFile = JavaFile.builder("foo", TypeSpec.classBuilder("Taco").build()) + .addFileComment("Pi\u00f1ata\u00a1") + .build(); + javaFile.writeTo(fsRoot); + + Path fooPath = fsRoot.resolve(fs.getPath("foo", "Taco.java")); + assertThat(new String(Files.readAllBytes(fooPath), UTF_8)).isEqualTo("" + + "// Pi\u00f1ata\u00a1\n" + + "package foo;\n" + + "\n" + + "class Taco {\n" + + "}\n"); + } +} diff --git a/src/test/java/com/squareup/javapoet/JavaFileTest.java b/src/test/java/com/squareup/javapoet/JavaFileTest.java new file mode 100644 index 0000000..e056116 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/JavaFileTest.java @@ -0,0 +1,692 @@ +/* + * 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.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.lang.model.element.Modifier; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(JUnit4.class) +public final class JavaFileTest { + @Test public void importStaticReadmeExample() { + ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard"); + ClassName namedBoards = ClassName.get("com.mattel", "Hoverboard", "Boards"); + ClassName list = ClassName.get("java.util", "List"); + ClassName arrayList = ClassName.get("java.util", "ArrayList"); + TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard); + MethodSpec beyond = MethodSpec.methodBuilder("beyond") + .returns(listOfHoverboards) + .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList) + .addStatement("result.add($T.createNimbus(2000))", hoverboard) + .addStatement("result.add($T.createNimbus(\"2001\"))", hoverboard) + .addStatement("result.add($T.createNimbus($T.THUNDERBOLT))", hoverboard, namedBoards) + .addStatement("$T.sort(result)", Collections.class) + .addStatement("return result.isEmpty() ? $T.emptyList() : result", Collections.class) + .build(); + TypeSpec hello = TypeSpec.classBuilder("HelloWorld") + .addMethod(beyond) + .build(); + JavaFile example = JavaFile.builder("com.example.helloworld", hello) + .addStaticImport(hoverboard, "createNimbus") + .addStaticImport(namedBoards, "*") + .addStaticImport(Collections.class, "*") + .build(); + assertThat(example.toString()).isEqualTo("" + + "package com.example.helloworld;\n" + + "\n" + + "import static com.mattel.Hoverboard.Boards.*;\n" + + "import static com.mattel.Hoverboard.createNimbus;\n" + + "import static java.util.Collections.*;\n" + + "\n" + + "import com.mattel.Hoverboard;\n" + + "import java.util.ArrayList;\n" + + "import java.util.List;\n" + + "\n" + + "class HelloWorld {\n" + + " List<Hoverboard> beyond() {\n" + + " List<Hoverboard> result = new ArrayList<>();\n" + + " result.add(createNimbus(2000));\n" + + " result.add(createNimbus(\"2001\"));\n" + + " result.add(createNimbus(THUNDERBOLT));\n" + + " sort(result);\n" + + " return result.isEmpty() ? emptyList() : result;\n" + + " }\n" + + "}\n"); + } + @Test public void importStaticForCrazyFormatsWorks() { + MethodSpec method = MethodSpec.methodBuilder("method").build(); + JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addStaticBlock(CodeBlock.builder() + .addStatement("$T", Runtime.class) + .addStatement("$T.a()", Runtime.class) + .addStatement("$T.X", Runtime.class) + .addStatement("$T$T", Runtime.class, Runtime.class) + .addStatement("$T.$T", Runtime.class, Runtime.class) + .addStatement("$1T$1T", Runtime.class) + .addStatement("$1T$2L$1T", Runtime.class, "?") + .addStatement("$1T$2L$2S$1T", Runtime.class, "?") + .addStatement("$1T$2L$2S$1T$3N$1T", Runtime.class, "?", method) + .addStatement("$T$L", Runtime.class, "?") + .addStatement("$T$S", Runtime.class, "?") + .addStatement("$T$N", Runtime.class, method) + .build()) + .build()) + .addStaticImport(Runtime.class, "*") + .build() + .toString(); // don't look at the generated code... + } + + @Test public void importStaticMixed() { + JavaFile source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addStaticBlock(CodeBlock.builder() + .addStatement("assert $1T.valueOf(\"BLOCKED\") == $1T.BLOCKED", Thread.State.class) + .addStatement("$T.gc()", System.class) + .addStatement("$1T.out.println($1T.nanoTime())", System.class) + .build()) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(Thread.State[].class, "states") + .varargs(true) + .build()) + .build()) + .addStaticImport(Thread.State.BLOCKED) + .addStaticImport(System.class, "*") + .addStaticImport(Thread.State.class, "valueOf") + .build(); + assertThat(source.toString()).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import static java.lang.System.*;\n" + + "import static java.lang.Thread.State.BLOCKED;\n" + + "import static java.lang.Thread.State.valueOf;\n" + + "\n" + + "import java.lang.Thread;\n" + + "\n" + + "class Taco {\n" + + " static {\n" + + " assert valueOf(\"BLOCKED\") == BLOCKED;\n" + + " gc();\n" + + " out.println(nanoTime());\n" + + " }\n" + + "\n" + + " Taco(Thread.State... states) {\n" + + " }\n" + + "}\n"); + } + + @Ignore("addStaticImport doesn't support members with $L") + @Test public void importStaticDynamic() { + JavaFile source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("main") + .addStatement("$T.$L.println($S)", System.class, "out", "hello") + .build()) + .build()) + .addStaticImport(System.class, "out") + .build(); + assertThat(source.toString()).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import static java.lang.System.out;\n" + + "\n" + + "class Taco {\n" + + " void main() {\n" + + " out.println(\"hello\");\n" + + " }\n" + + "}\n"); + } + + @Test public void importStaticNone() { + assertThat(JavaFile.builder("readme", importStaticTypeSpec("Util")) + .build().toString()).isEqualTo("" + + "package readme;\n" + + "\n" + + "import java.lang.System;\n" + + "import java.util.concurrent.TimeUnit;\n" + + "\n" + + "class Util {\n" + + " public static long minutesToSeconds(long minutes) {\n" + + " System.gc();\n" + + " return TimeUnit.SECONDS.convert(minutes, TimeUnit.MINUTES);\n" + + " }\n" + + "}\n"); + } + + @Test public void importStaticOnce() { + assertThat(JavaFile.builder("readme", importStaticTypeSpec("Util")) + .addStaticImport(TimeUnit.SECONDS) + .build().toString()).isEqualTo("" + + "package readme;\n" + + "\n" + + "import static java.util.concurrent.TimeUnit.SECONDS;\n" + + "\n" + + "import java.lang.System;\n" + + "import java.util.concurrent.TimeUnit;\n" + + "\n" + + "class Util {\n" + + " public static long minutesToSeconds(long minutes) {\n" + + " System.gc();\n" + + " return SECONDS.convert(minutes, TimeUnit.MINUTES);\n" + + " }\n" + + "}\n"); + } + + @Test public void importStaticTwice() { + assertThat(JavaFile.builder("readme", importStaticTypeSpec("Util")) + .addStaticImport(TimeUnit.SECONDS) + .addStaticImport(TimeUnit.MINUTES) + .build().toString()).isEqualTo("" + + "package readme;\n" + + "\n" + + "import static java.util.concurrent.TimeUnit.MINUTES;\n" + + "import static java.util.concurrent.TimeUnit.SECONDS;\n" + + "\n" + + "import java.lang.System;\n" + + "\n" + + "class Util {\n" + + " public static long minutesToSeconds(long minutes) {\n" + + " System.gc();\n" + + " return SECONDS.convert(minutes, MINUTES);\n" + + " }\n" + + "}\n"); + } + + @Test public void importStaticUsingWildcards() { + assertThat(JavaFile.builder("readme", importStaticTypeSpec("Util")) + .addStaticImport(TimeUnit.class, "*") + .addStaticImport(System.class, "*") + .build().toString()).isEqualTo("" + + "package readme;\n" + + "\n" + + "import static java.lang.System.*;\n" + + "import static java.util.concurrent.TimeUnit.*;\n" + + "\n" + + "class Util {\n" + + " public static long minutesToSeconds(long minutes) {\n" + + " gc();\n" + + " return SECONDS.convert(minutes, MINUTES);\n" + + " }\n" + + "}\n"); + } + + private TypeSpec importStaticTypeSpec(String name) { + MethodSpec method = MethodSpec.methodBuilder("minutesToSeconds") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(long.class) + .addParameter(long.class, "minutes") + .addStatement("$T.gc()", System.class) + .addStatement("return $1T.SECONDS.convert(minutes, $1T.MINUTES)", TimeUnit.class) + .build(); + return TypeSpec.classBuilder(name).addMethod(method).build(); + + } + @Test public void noImports() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco").build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void singleImport() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addField(Date.class, "madeFreshDate") + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.util.Date;\n" + + "\n" + + "class Taco {\n" + + " Date madeFreshDate;\n" + + "}\n"); + } + + @Test public void conflictingImports() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addField(Date.class, "madeFreshDate") + .addField(ClassName.get("java.sql", "Date"), "madeFreshDatabaseDate") + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.util.Date;\n" + + "\n" + + "class Taco {\n" + + " Date madeFreshDate;\n" + + "\n" + + " java.sql.Date madeFreshDatabaseDate;\n" + + "}\n"); + } + + @Test public void annotatedTypeParam() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addField(ParameterizedTypeName.get(ClassName.get(List.class), + ClassName.get("com.squareup.meat", "Chorizo") + .annotated(AnnotationSpec.builder(ClassName.get("com.squareup.tacos", "Spicy")) + .build())), "chorizo") + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.meat.Chorizo;\n" + + "import java.util.List;\n" + + "\n" + + "class Taco {\n" + + " List<@Spicy Chorizo> chorizo;\n" + + "}\n"); + } + + @Test public void skipJavaLangImportsWithConflictingClassLast() throws Exception { + // Whatever is used first wins! In this case the Float in java.lang is imported. + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addField(ClassName.get("java.lang", "Float"), "litres") + .addField(ClassName.get("com.squareup.soda", "Float"), "beverage") + .build()) + .skipJavaLangImports(true) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " Float litres;\n" + + "\n" + + " com.squareup.soda.Float beverage;\n" // Second 'Float' is fully qualified. + + "}\n"); + } + + @Test public void skipJavaLangImportsWithConflictingClassFirst() throws Exception { + // Whatever is used first wins! In this case the Float in com.squareup.soda is imported. + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addField(ClassName.get("com.squareup.soda", "Float"), "beverage") + .addField(ClassName.get("java.lang", "Float"), "litres") + .build()) + .skipJavaLangImports(true) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.soda.Float;\n" + + "\n" + + "class Taco {\n" + + " Float beverage;\n" + + "\n" + + " java.lang.Float litres;\n" // Second 'Float' is fully qualified. + + "}\n"); + } + + @Test public void conflictingParentName() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("A") + .addType(TypeSpec.classBuilder("B") + .addType(TypeSpec.classBuilder("Twin").build()) + .addType(TypeSpec.classBuilder("C") + .addField(ClassName.get("com.squareup.tacos", "A", "Twin", "D"), "d") + .build()) + .build()) + .addType(TypeSpec.classBuilder("Twin") + .addType(TypeSpec.classBuilder("D") + .build()) + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class A {\n" + + " class B {\n" + + " class Twin {\n" + + " }\n" + + "\n" + + " class C {\n" + + " A.Twin.D d;\n" + + " }\n" + + " }\n" + + "\n" + + " class Twin {\n" + + " class D {\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void conflictingChildName() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("A") + .addType(TypeSpec.classBuilder("B") + .addType(TypeSpec.classBuilder("C") + .addField(ClassName.get("com.squareup.tacos", "A", "Twin", "D"), "d") + .addType(TypeSpec.classBuilder("Twin").build()) + .build()) + .build()) + .addType(TypeSpec.classBuilder("Twin") + .addType(TypeSpec.classBuilder("D") + .build()) + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class A {\n" + + " class B {\n" + + " class C {\n" + + " A.Twin.D d;\n" + + "\n" + + " class Twin {\n" + + " }\n" + + " }\n" + + " }\n" + + "\n" + + " class Twin {\n" + + " class D {\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void conflictingNameOutOfScope() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("A") + .addType(TypeSpec.classBuilder("B") + .addType(TypeSpec.classBuilder("C") + .addField(ClassName.get("com.squareup.tacos", "A", "Twin", "D"), "d") + .addType(TypeSpec.classBuilder("Nested") + .addType(TypeSpec.classBuilder("Twin").build()) + .build()) + .build()) + .build()) + .addType(TypeSpec.classBuilder("Twin") + .addType(TypeSpec.classBuilder("D") + .build()) + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class A {\n" + + " class B {\n" + + " class C {\n" + + " Twin.D d;\n" + + "\n" + + " class Nested {\n" + + " class Twin {\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "\n" + + " class Twin {\n" + + " class D {\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void nestedClassAndSuperclassShareName() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .superclass(ClassName.get("com.squareup.wire", "Message")) + .addType(TypeSpec.classBuilder("Builder") + .superclass(ClassName.get("com.squareup.wire", "Message", "Builder")) + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.wire.Message;\n" + + "\n" + + "class Taco extends Message {\n" + + " class Builder extends Message.Builder {\n" + + " }\n" + + "}\n"); + } + + @Test public void classAndSuperclassShareName() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .superclass(ClassName.get("com.taco.bell", "Taco")) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco extends com.taco.bell.Taco {\n" + + "}\n"); + } + + @Test public void conflictingAnnotation() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addAnnotation(ClassName.get("com.taco.bell", "Taco")) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "@com.taco.bell.Taco\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void conflictingAnnotationReferencedClass() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addAnnotation(AnnotationSpec.builder(ClassName.get("com.squareup.tacos", "MyAnno")) + .addMember("value", "$T.class", ClassName.get("com.taco.bell", "Taco")) + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "@MyAnno(com.taco.bell.Taco.class)\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void conflictingTypeVariableBound() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addTypeVariable( + TypeVariableName.get("T", ClassName.get("com.taco.bell", "Taco"))) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco<T extends com.taco.bell.Taco> {\n" + + "}\n"); + } + + @Test public void superclassReferencesSelf() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .superclass(ParameterizedTypeName.get( + ClassName.get(Comparable.class), ClassName.get("com.squareup.tacos", "Taco"))) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Comparable;\n" + + "\n" + + "class Taco extends Comparable<Taco> {\n" + + "}\n"); + } + + /** https://github.com/square/javapoet/issues/366 */ + @Test public void annotationIsNestedClass() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("TestComponent") + .addAnnotation(ClassName.get("dagger", "Component")) + .addType(TypeSpec.classBuilder("Builder") + .addAnnotation(ClassName.get("dagger", "Component", "Builder")) + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import dagger.Component;\n" + + "\n" + + "@Component\n" + + "class TestComponent {\n" + + " @Component.Builder\n" + + " class Builder {\n" + + " }\n" + + "}\n"); + } + + @Test public void defaultPackage() throws Exception { + String source = JavaFile.builder("", + TypeSpec.classBuilder("HelloWorld") + .addMethod(MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(String[].class, "args") + .addCode("$T.out.println($S);\n", System.class, "Hello World!") + .build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "import java.lang.String;\n" + + "import java.lang.System;\n" + + "\n" + + "class HelloWorld {\n" + + " public static void main(String[] args) {\n" + + " System.out.println(\"Hello World!\");\n" + + " }\n" + + "}\n"); + } + + @Test public void defaultPackageTypesAreNotImported() throws Exception { + String source = JavaFile.builder("hello", + TypeSpec.classBuilder("World").addSuperinterface(ClassName.get("", "Test")).build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package hello;\n" + + "\n" + + "class World implements Test {\n" + + "}\n"); + } + + @Test public void topOfFileComment() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco").build()) + .addFileComment("Generated $L by JavaPoet. DO NOT EDIT!", "2015-01-13") + .build() + .toString(); + assertThat(source).isEqualTo("" + + "// Generated 2015-01-13 by JavaPoet. DO NOT EDIT!\n" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void emptyLinesInTopOfFileComment() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco").build()) + .addFileComment("\nGENERATED FILE:\n\nDO NOT EDIT!\n") + .build() + .toString(); + assertThat(source).isEqualTo("" + + "//\n" + + "// GENERATED FILE:\n" + + "//\n" + + "// DO NOT EDIT!\n" + + "//\n" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void packageClassConflictsWithNestedClass() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .addField(ClassName.get("com.squareup.tacos", "A"), "a") + .addType(TypeSpec.classBuilder("A").build()) + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " com.squareup.tacos.A a;\n" + + "\n" + + " class A {\n" + + " }\n" + + "}\n"); + } + + @Test public void packageClassConflictsWithSuperlass() throws Exception { + String source = JavaFile.builder("com.squareup.tacos", + TypeSpec.classBuilder("Taco") + .superclass(ClassName.get("com.taco.bell", "A")) + .addField(ClassName.get("com.squareup.tacos", "A"), "a") + .build()) + .build() + .toString(); + assertThat(source).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco extends com.taco.bell.A {\n" + + " A a;\n" + + "}\n"); + } +} diff --git a/src/test/java/com/squareup/javapoet/LineWrapperTest.java b/src/test/java/com/squareup/javapoet/LineWrapperTest.java new file mode 100644 index 0000000..ba8472c --- /dev/null +++ b/src/test/java/com/squareup/javapoet/LineWrapperTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(JUnit4.class) +public final class LineWrapperTest { + @Test public void wrap() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("fghij"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde\n fghij"); + } + + @Test public void noWrap() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("fghi"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde fghi"); + } + + @Test public void zeroWidthNoWrap() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.zeroWidthSpace(2); + lineWrapper.append("fghij"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcdefghij"); + } + + @Test public void nospaceWrapMax() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.zeroWidthSpace(2); + lineWrapper.append("fghijk"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde\n fghijk"); + } + + @Test public void multipleWrite() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("ab"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("cd"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("ef"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("gh"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("ij"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("kl"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("mn"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("op"); + lineWrapper.wrappingSpace(1); + lineWrapper.append("qr"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("ab cd ef\n gh ij kl\n mn op qr"); + } + + @Test public void fencepost() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.append("fghij"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("k"); + lineWrapper.append("lmnop"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcdefghij\n klmnop"); + } + + @Test public void fencepostZeroWidth() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.append("fghij"); + lineWrapper.zeroWidthSpace(2); + lineWrapper.append("k"); + lineWrapper.append("lmnop"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcdefghij\n klmnop"); + } + + @Test public void overlyLongLinesWithoutLeadingSpace() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcdefghijkl"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcdefghijkl"); + } + + @Test public void overlyLongLinesWithLeadingSpace() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.wrappingSpace(2); + lineWrapper.append("abcdefghijkl"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("\n abcdefghijkl"); + } + + @Test public void overlyLongLinesWithLeadingZeroWidth() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.zeroWidthSpace(2); + lineWrapper.append("abcdefghijkl"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcdefghijkl"); + } + + @Test public void noWrapEmbeddedNewlines() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("fghi\njklmn"); + lineWrapper.append("opqrstuvwxy"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde fghi\njklmnopqrstuvwxy"); + } + + @Test public void wrapEmbeddedNewlines() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("fghij\nklmn"); + lineWrapper.append("opqrstuvwxy"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde\n fghij\nklmnopqrstuvwxy"); + } + + @Test public void noWrapEmbeddedNewlines_ZeroWidth() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.zeroWidthSpace(2); + lineWrapper.append("fghij\nklmn"); + lineWrapper.append("opqrstuvwxyz"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcdefghij\nklmnopqrstuvwxyz"); + } + + @Test public void wrapEmbeddedNewlines_ZeroWidth() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.zeroWidthSpace(2); + lineWrapper.append("fghijk\nlmn"); + lineWrapper.append("opqrstuvwxy"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde\n fghijk\nlmnopqrstuvwxy"); + } + + @Test public void noWrapMultipleNewlines() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("fghi\nklmnopq\nr"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("stuvwxyz"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde fghi\nklmnopq\nr stuvwxyz"); + } + + @Test public void wrapMultipleNewlines() throws Exception { + StringBuffer out = new StringBuffer(); + LineWrapper lineWrapper = new LineWrapper(out, " ", 10); + lineWrapper.append("abcde"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("fghi\nklmnopq\nrs"); + lineWrapper.wrappingSpace(2); + lineWrapper.append("tuvwxyz1"); + lineWrapper.close(); + assertThat(out.toString()).isEqualTo("abcde fghi\nklmnopq\nrs\n tuvwxyz1"); + } +} diff --git a/src/test/java/com/squareup/javapoet/MethodSpecTest.java b/src/test/java/com/squareup/javapoet/MethodSpecTest.java new file mode 100644 index 0000000..5dfabaa --- /dev/null +++ b/src/test/java/com/squareup/javapoet/MethodSpecTest.java @@ -0,0 +1,305 @@ +/* + * 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 com.google.testing.compile.CompilationRule; +import java.io.Closeable; +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeoutException; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertThat; +import static javax.lang.model.util.ElementFilter.methodsIn; +import static org.junit.Assert.fail; + +public final class MethodSpecTest { + @Rule public final CompilationRule compilation = new CompilationRule(); + + private Elements elements; + private Types types; + + @Before public void setUp() { + elements = compilation.getElements(); + types = compilation.getTypes(); + } + + private TypeElement getElement(Class<?> clazz) { + return elements.getTypeElement(clazz.getCanonicalName()); + } + + private ExecutableElement findFirst(Collection<ExecutableElement> elements, String name) { + for (ExecutableElement executableElement : elements) { + if (executableElement.getSimpleName().toString().equals(name)) { + return executableElement; + } + } + throw new IllegalArgumentException(name + " not found in " + elements); + } + + @Test public void nullAnnotationsAddition() { + try { + MethodSpec.methodBuilder("doSomething").addAnnotations(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("annotationSpecs == null"); + } + } + + @Test public void nullTypeVariablesAddition() { + try { + MethodSpec.methodBuilder("doSomething").addTypeVariables(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("typeVariables == null"); + } + } + + @Test public void nullParametersAddition() { + try { + MethodSpec.methodBuilder("doSomething").addParameters(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("parameterSpecs == null"); + } + } + + @Test public void nullExceptionsAddition() { + try { + MethodSpec.methodBuilder("doSomething").addExceptions(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("exceptions == null"); + } + } + + @Target(ElementType.PARAMETER) + @interface Nullable { + } + + abstract static class Everything { + @Deprecated protected abstract <T extends Runnable & Closeable> Runnable everything( + @Nullable String thing, List<? extends T> things) throws IOException, SecurityException; + } + + abstract static class Generics { + <T, R, V extends Throwable> T run(R param) throws V { + return null; + } + } + + abstract static class HasAnnotation { + @Override public abstract String toString(); + } + + interface Throws<R extends RuntimeException> { + void fail() throws R; + } + + interface ExtendsOthers extends Callable<Integer>, Comparable<ExtendsOthers>, + Throws<IllegalStateException> { + } + + interface ExtendsIterableWithDefaultMethods extends Iterable<Object> { + } + + final class FinalClass { + void method() { + } + } + + abstract static class InvalidOverrideMethods { + final void finalMethod() { + } + + private void privateMethod() { + } + + static void staticMethod() { + } + } + + @Test public void overrideEverything() { + TypeElement classElement = getElement(Everything.class); + ExecutableElement methodElement = getOnlyElement(methodsIn(classElement.getEnclosedElements())); + MethodSpec method = MethodSpec.overriding(methodElement).build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "protected <T extends java.lang.Runnable & java.io.Closeable> java.lang.Runnable " + + "everything(\n" + + " java.lang.String arg0, java.util.List<? extends T> arg1) throws java.io.IOException,\n" + + " java.lang.SecurityException {\n" + + "}\n"); + } + + @Test public void overrideGenerics() { + TypeElement classElement = getElement(Generics.class); + ExecutableElement methodElement = getOnlyElement(methodsIn(classElement.getEnclosedElements())); + MethodSpec method = MethodSpec.overriding(methodElement) + .addStatement("return null") + .build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "<T, R, V extends java.lang.Throwable> T run(R param) throws V {\n" + + " return null;\n" + + "}\n"); + } + + @Test public void overrideDoesNotCopyOverrideAnnotation() { + TypeElement classElement = getElement(HasAnnotation.class); + ExecutableElement exec = getOnlyElement(methodsIn(classElement.getEnclosedElements())); + MethodSpec method = MethodSpec.overriding(exec).build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "public java.lang.String toString() {\n" + + "}\n"); + } + + @Test public void overrideDoesNotCopyDefaultModifier() { + TypeElement classElement = getElement(ExtendsIterableWithDefaultMethods.class); + DeclaredType classType = (DeclaredType) classElement.asType(); + List<ExecutableElement> methods = methodsIn(elements.getAllMembers(classElement)); + ExecutableElement exec = findFirst(methods, "spliterator"); + MethodSpec method = MethodSpec.overriding(exec, classType, types).build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "public java.util.Spliterator<java.lang.Object> spliterator() {\n" + + "}\n"); + } + + @Test public void overrideExtendsOthersWorksWithActualTypeParameters() { + TypeElement classElement = getElement(ExtendsOthers.class); + DeclaredType classType = (DeclaredType) classElement.asType(); + List<ExecutableElement> methods = methodsIn(elements.getAllMembers(classElement)); + ExecutableElement exec = findFirst(methods, "call"); + MethodSpec method = MethodSpec.overriding(exec, classType, types).build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "public java.lang.Integer call() throws java.lang.Exception {\n" + + "}\n"); + exec = findFirst(methods, "compareTo"); + method = MethodSpec.overriding(exec, classType, types).build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "public int compareTo(" + ExtendsOthers.class.getCanonicalName() + " arg0) {\n" + + "}\n"); + exec = findFirst(methods, "fail"); + method = MethodSpec.overriding(exec, classType, types).build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "public void fail() throws java.lang.IllegalStateException {\n" + + "}\n"); + } + + @Test public void overrideFinalClassMethod() { + TypeElement classElement = getElement(FinalClass.class); + List<ExecutableElement> methods = methodsIn(elements.getAllMembers(classElement)); + try { + MethodSpec.overriding(findFirst(methods, "method")); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo( + "Cannot override method on final class com.squareup.javapoet.MethodSpecTest.FinalClass"); + } + } + + @Test public void overrideInvalidModifiers() { + TypeElement classElement = getElement(InvalidOverrideMethods.class); + List<ExecutableElement> methods = methodsIn(elements.getAllMembers(classElement)); + try { + MethodSpec.overriding(findFirst(methods, "finalMethod")); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("cannot override method with modifiers: [final]"); + } + try { + MethodSpec.overriding(findFirst(methods, "privateMethod")); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("cannot override method with modifiers: [private]"); + } + try { + MethodSpec.overriding(findFirst(methods, "staticMethod")); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("cannot override method with modifiers: [static]"); + } + } + + @Test public void equalsAndHashCode() { + MethodSpec a = MethodSpec.constructorBuilder().build(); + MethodSpec b = MethodSpec.constructorBuilder().build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = MethodSpec.methodBuilder("taco").build(); + b = MethodSpec.methodBuilder("taco").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + TypeElement classElement = getElement(Everything.class); + ExecutableElement methodElement = getOnlyElement(methodsIn(classElement.getEnclosedElements())); + a = MethodSpec.overriding(methodElement).build(); + b = MethodSpec.overriding(methodElement).build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test public void duplicateExceptionsIgnored() { + ClassName ioException = ClassName.get(IOException.class); + ClassName timeoutException = ClassName.get(TimeoutException.class); + MethodSpec methodSpec = MethodSpec.methodBuilder("duplicateExceptions") + .addException(ioException) + .addException(timeoutException) + .addException(timeoutException) + .addException(ioException) + .build(); + assertThat(methodSpec.exceptions).isEqualTo(Arrays.asList(ioException, timeoutException)); + assertThat(methodSpec.toBuilder().addException(ioException).build().exceptions) + .isEqualTo(Arrays.asList(ioException, timeoutException)); + } + + @Test public void nullIsNotAValidMethodName() { + try { + MethodSpec.methodBuilder(null); + fail("NullPointerException expected"); + } catch (NullPointerException e) { + assertThat(e.getMessage()).isEqualTo("name == null"); + } + } + + @Test public void addModifiersVarargsShouldNotBeNull() { + try { + MethodSpec.methodBuilder("taco") + .addModifiers((Modifier[]) null); + fail("NullPointerException expected"); + } catch (NullPointerException e) { + assertThat(e.getMessage()).isEqualTo("modifiers == null"); + } + } +} diff --git a/src/test/java/com/squareup/javapoet/NameAllocatorTest.java b/src/test/java/com/squareup/javapoet/NameAllocatorTest.java new file mode 100644 index 0000000..1840107 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/NameAllocatorTest.java @@ -0,0 +1,108 @@ +/* + * 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 org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +public final class NameAllocatorTest { + @Test public void usage() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("foo", 1)).isEqualTo("foo"); + assertThat(nameAllocator.newName("bar", 2)).isEqualTo("bar"); + assertThat(nameAllocator.get(1)).isEqualTo("foo"); + assertThat(nameAllocator.get(2)).isEqualTo("bar"); + } + + @Test public void nameCollision() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("foo")).isEqualTo("foo"); + assertThat(nameAllocator.newName("foo")).isEqualTo("foo_"); + assertThat(nameAllocator.newName("foo")).isEqualTo("foo__"); + } + + @Test public void nameCollisionWithTag() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("foo", 1)).isEqualTo("foo"); + assertThat(nameAllocator.newName("foo", 2)).isEqualTo("foo_"); + assertThat(nameAllocator.newName("foo", 3)).isEqualTo("foo__"); + assertThat(nameAllocator.get(1)).isEqualTo("foo"); + assertThat(nameAllocator.get(2)).isEqualTo("foo_"); + assertThat(nameAllocator.get(3)).isEqualTo("foo__"); + } + + @Test public void characterMappingSubstitute() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("a-b", 1)).isEqualTo("a_b"); + } + + @Test public void characterMappingSurrogate() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("a\uD83C\uDF7Ab", 1)).isEqualTo("a_b"); + } + + @Test public void characterMappingInvalidStartButValidPart() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("1ab", 1)).isEqualTo("_1ab"); + } + + @Test public void characterMappingInvalidStartIsInvalidPart() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("&ab", 1)).isEqualTo("_ab"); + } + + @Test public void javaKeyword() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + assertThat(nameAllocator.newName("public", 1)).isEqualTo("public_"); + assertThat(nameAllocator.get(1)).isEqualTo("public_"); + } + + @Test public void tagReuseForbidden() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + nameAllocator.newName("foo", 1); + try { + nameAllocator.newName("bar", 1); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("tag 1 cannot be used for both 'foo' and 'bar'"); + } + } + + @Test public void useBeforeAllocateForbidden() throws Exception { + NameAllocator nameAllocator = new NameAllocator(); + try { + nameAllocator.get(1); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("unknown tag: 1"); + } + } + + @Test public void cloneUsage() throws Exception { + NameAllocator outterAllocator = new NameAllocator(); + outterAllocator.newName("foo", 1); + + NameAllocator innerAllocator1 = outterAllocator.clone(); + assertThat(innerAllocator1.newName("bar", 2)).isEqualTo("bar"); + assertThat(innerAllocator1.newName("foo", 3)).isEqualTo("foo_"); + + NameAllocator innerAllocator2 = outterAllocator.clone(); + assertThat(innerAllocator2.newName("foo", 2)).isEqualTo("foo_"); + assertThat(innerAllocator2.newName("bar", 3)).isEqualTo("bar"); + } +} diff --git a/src/test/java/com/squareup/javapoet/ParameterSpecTest.java b/src/test/java/com/squareup/javapoet/ParameterSpecTest.java new file mode 100644 index 0000000..2f81866 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/ParameterSpecTest.java @@ -0,0 +1,46 @@ +/* + * 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 org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import javax.lang.model.element.Modifier; + +public class ParameterSpecTest { + @Test public void equalsAndHashCode() { + ParameterSpec a = ParameterSpec.builder(int.class, "foo").build(); + ParameterSpec b = ParameterSpec.builder(int.class, "foo").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = ParameterSpec.builder(int.class, "i").addModifiers(Modifier.STATIC).build(); + b = ParameterSpec.builder(int.class, "i").addModifiers(Modifier.STATIC).build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test public void nullAnnotationsAddition() { + try { + ParameterSpec.builder(int.class, "foo").addAnnotations(null); + fail(); + } catch (Exception e) { + assertThat(e.getMessage()) + .isEqualTo("annotationSpecs == null"); + } + } +}
\ No newline at end of file diff --git a/src/test/java/com/squareup/javapoet/TestFiler.java b/src/test/java/com/squareup/javapoet/TestFiler.java new file mode 100644 index 0000000..274877c --- /dev/null +++ b/src/test/java/com/squareup/javapoet/TestFiler.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 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.io.OutputStream; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.Filer; +import javax.lang.model.element.Element; +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; + +final class TestFiler implements Filer { + class Source extends SimpleJavaFileObject { + private final Path path; + protected Source(Path path) { + super(path.toUri(), Kind.SOURCE); + this.path = path; + } + @Override public OutputStream openOutputStream() throws IOException { + Path parent = path.getParent(); + if (!Files.exists(parent)) fileSystemProvider.createDirectory(parent); + return fileSystemProvider.newOutputStream(path); + } + } + + private final String separator; + private final Path fileSystemRoot; + private final FileSystemProvider fileSystemProvider; + private final Map<Path, Set<Element>> originatingElementsMap; + + public TestFiler(FileSystem fileSystem, Path fsRoot) { + separator = fileSystem.getSeparator(); + fileSystemRoot = fsRoot; + fileSystemProvider = fileSystem.provider(); + originatingElementsMap = new LinkedHashMap<>(); + } + + public Set<Element> getOriginatingElements(Path path) { + return originatingElementsMap.get(path); + } + + @Override public JavaFileObject createSourceFile( + CharSequence name, Element... originatingElements) throws IOException { + String relative = name.toString().replace(".", separator) + ".java"; // Assumes well-formed. + Path path = fileSystemRoot.resolve(relative); + originatingElementsMap.put(path, Util.immutableSet(Arrays.asList(originatingElements))); + return new Source(path); + } + + @Override public JavaFileObject createClassFile(CharSequence name, Element... originatingElements) + throws IOException { + throw new UnsupportedOperationException("Not implemented."); + } + + @Override public FileObject createResource(JavaFileManager.Location location, CharSequence pkg, + CharSequence relativeName, Element... originatingElements) throws IOException { + throw new UnsupportedOperationException("Not implemented."); + } + + @Override public FileObject getResource(JavaFileManager.Location location, CharSequence pkg, + CharSequence relativeName) throws IOException { + throw new UnsupportedOperationException("Not implemented."); + } +} diff --git a/src/test/java/com/squareup/javapoet/TypeNameTest.java b/src/test/java/com/squareup/javapoet/TypeNameTest.java new file mode 100644 index 0000000..99ed58d --- /dev/null +++ b/src/test/java/com/squareup/javapoet/TypeNameTest.java @@ -0,0 +1,184 @@ +/* + * 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.Serializable; +import java.lang.reflect.Method; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; + +public class TypeNameTest { + + protected <E extends Enum<E>> E generic(E[] values) { + return values[0]; + } + + protected static class TestGeneric<T> { + class Inner {} + + class InnerGeneric<T2> {} + + static class NestedNonGeneric {} + } + + protected static TestGeneric<String>.Inner testGenericStringInner() { + return null; + } + + protected static TestGeneric<Integer>.Inner testGenericIntInner() { + return null; + } + + protected static TestGeneric<Short>.InnerGeneric<Long> testGenericInnerLong() { + return null; + } + + protected static TestGeneric<Short>.InnerGeneric<Integer> testGenericInnerInt() { + return null; + } + + protected static TestGeneric.NestedNonGeneric testNestedNonGeneric() { + return null; + } + + @Test public void genericType() throws Exception { + Method recursiveEnum = getClass().getDeclaredMethod("generic", Enum[].class); + TypeName.get(recursiveEnum.getReturnType()); + TypeName.get(recursiveEnum.getGenericReturnType()); + TypeName genericTypeName = TypeName.get(recursiveEnum.getParameterTypes()[0]); + TypeName.get(recursiveEnum.getGenericParameterTypes()[0]); + + // Make sure the generic argument is present + assertThat(genericTypeName.toString()).contains("Enum"); + } + + @Test public void innerClassInGenericType() throws Exception { + Method genericStringInner = getClass().getDeclaredMethod("testGenericStringInner"); + TypeName.get(genericStringInner.getReturnType()); + TypeName genericTypeName = TypeName.get(genericStringInner.getGenericReturnType()); + assertNotEquals(TypeName.get(genericStringInner.getGenericReturnType()), + TypeName.get(getClass().getDeclaredMethod("testGenericIntInner").getGenericReturnType())); + + // Make sure the generic argument is present + assertThat(genericTypeName.toString()).isEqualTo( + TestGeneric.class.getCanonicalName() + "<java.lang.String>.Inner"); + } + + @Test public void innerGenericInGenericType() throws Exception { + Method genericStringInner = getClass().getDeclaredMethod("testGenericInnerLong"); + TypeName.get(genericStringInner.getReturnType()); + TypeName genericTypeName = TypeName.get(genericStringInner.getGenericReturnType()); + assertNotEquals(TypeName.get(genericStringInner.getGenericReturnType()), + TypeName.get(getClass().getDeclaredMethod("testGenericInnerInt").getGenericReturnType())); + + // Make sure the generic argument is present + assertThat(genericTypeName.toString()).isEqualTo( + TestGeneric.class.getCanonicalName() + "<java.lang.Short>.InnerGeneric<java.lang.Long>"); + } + + @Test public void innerStaticInGenericType() throws Exception { + Method staticInGeneric = getClass().getDeclaredMethod("testNestedNonGeneric"); + TypeName.get(staticInGeneric.getReturnType()); + TypeName typeName = TypeName.get(staticInGeneric.getGenericReturnType()); + + // Make sure there are no generic arguments + assertThat(typeName.toString()).isEqualTo( + TestGeneric.class.getCanonicalName() + ".NestedNonGeneric"); + } + + @Test public void equalsAndHashCodePrimitive() { + assertEqualsHashCodeAndToString(TypeName.BOOLEAN, TypeName.BOOLEAN); + assertEqualsHashCodeAndToString(TypeName.BYTE, TypeName.BYTE); + assertEqualsHashCodeAndToString(TypeName.CHAR, TypeName.CHAR); + assertEqualsHashCodeAndToString(TypeName.DOUBLE, TypeName.DOUBLE); + assertEqualsHashCodeAndToString(TypeName.FLOAT, TypeName.FLOAT); + assertEqualsHashCodeAndToString(TypeName.INT, TypeName.INT); + assertEqualsHashCodeAndToString(TypeName.LONG, TypeName.LONG); + assertEqualsHashCodeAndToString(TypeName.SHORT, TypeName.SHORT); + assertEqualsHashCodeAndToString(TypeName.VOID, TypeName.VOID); + } + + @Test public void equalsAndHashCodeArrayTypeName() { + assertEqualsHashCodeAndToString(ArrayTypeName.of(Object.class), + ArrayTypeName.of(Object.class)); + assertEqualsHashCodeAndToString(TypeName.get(Object[].class), + ArrayTypeName.of(Object.class)); + } + + @Test public void equalsAndHashCodeClassName() { + assertEqualsHashCodeAndToString(ClassName.get(Object.class), ClassName.get(Object.class)); + assertEqualsHashCodeAndToString(TypeName.get(Object.class), ClassName.get(Object.class)); + assertEqualsHashCodeAndToString(ClassName.bestGuess("java.lang.Object"), + ClassName.get(Object.class)); + } + + @Test public void equalsAndHashCodeParameterizedTypeName() { + assertEqualsHashCodeAndToString(ParameterizedTypeName.get(Object.class), + ParameterizedTypeName.get(Object.class)); + assertEqualsHashCodeAndToString(ParameterizedTypeName.get(Set.class, UUID.class), + ParameterizedTypeName.get(Set.class, UUID.class)); + assertNotEquals(ClassName.get(List.class), ParameterizedTypeName.get(List.class, + String.class)); + } + + @Test public void equalsAndHashCodeTypeVariableName() { + assertEqualsHashCodeAndToString(TypeVariableName.get(Object.class), + TypeVariableName.get(Object.class)); + TypeVariableName typeVar1 = TypeVariableName.get("T", Comparator.class, Serializable.class); + TypeVariableName typeVar2 = TypeVariableName.get("T", Comparator.class, Serializable.class); + assertEqualsHashCodeAndToString(typeVar1, typeVar2); + } + + @Test public void equalsAndHashCodeWildcardTypeName() { + assertEqualsHashCodeAndToString(WildcardTypeName.subtypeOf(Object.class), + WildcardTypeName.subtypeOf(Object.class)); + assertEqualsHashCodeAndToString(WildcardTypeName.subtypeOf(Serializable.class), + WildcardTypeName.subtypeOf(Serializable.class)); + assertEqualsHashCodeAndToString(WildcardTypeName.supertypeOf(String.class), + WildcardTypeName.supertypeOf(String.class)); + } + + @Test public void isPrimitive() throws Exception { + assertThat(TypeName.INT.isPrimitive()).isTrue(); + assertThat(ClassName.get("java.lang", "Integer").isPrimitive()).isFalse(); + assertThat(ClassName.get("java.lang", "String").isPrimitive()).isFalse(); + assertThat(TypeName.VOID.isPrimitive()).isFalse(); + assertThat(ClassName.get("java.lang", "Void").isPrimitive()).isFalse(); + } + + @Test public void isBoxedPrimitive() throws Exception { + assertThat(TypeName.INT.isBoxedPrimitive()).isFalse(); + assertThat(ClassName.get("java.lang", "Integer").isBoxedPrimitive()).isTrue(); + assertThat(ClassName.get("java.lang", "String").isBoxedPrimitive()).isFalse(); + assertThat(TypeName.VOID.isBoxedPrimitive()).isFalse(); + assertThat(ClassName.get("java.lang", "Void").isBoxedPrimitive()).isFalse(); + } + + private void assertEqualsHashCodeAndToString(TypeName a, TypeName b) { + assertEquals(a.toString(), b.toString()); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + assertFalse(a.equals(null)); + } +} diff --git a/src/test/java/com/squareup/javapoet/TypeSpecTest.java b/src/test/java/com/squareup/javapoet/TypeSpecTest.java new file mode 100644 index 0000000..9cd22c2 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/TypeSpecTest.java @@ -0,0 +1,2359 @@ +/* + * 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 com.google.common.collect.ImmutableMap; +import com.google.testing.compile.CompilationRule; +import java.io.IOException; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.EventListener; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public final class TypeSpecTest { + private final String tacosPackage = "com.squareup.tacos"; + private static final String donutsPackage = "com.squareup.donuts"; + + @Rule public final CompilationRule compilation = new CompilationRule(); + + private TypeElement getElement(Class<?> clazz) { + return compilation.getElements().getTypeElement(clazz.getCanonicalName()); + } + + @Test public void basic() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .returns(String.class) + .addCode("return $S;\n", "taco") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " @Override\n" + + " public final String toString() {\n" + + " return \"taco\";\n" + + " }\n" + + "}\n"); + assertEquals(472949424, taco.hashCode()); // update expected number if source changes + } + + @Test public void interestingTypes() throws Exception { + TypeName listOfAny = ParameterizedTypeName.get( + ClassName.get(List.class), WildcardTypeName.subtypeOf(Object.class)); + TypeName listOfExtends = ParameterizedTypeName.get( + ClassName.get(List.class), WildcardTypeName.subtypeOf(Serializable.class)); + TypeName listOfSuper = ParameterizedTypeName.get(ClassName.get(List.class), + WildcardTypeName.supertypeOf(String.class)); + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(listOfAny, "extendsObject") + .addField(listOfExtends, "extendsSerializable") + .addField(listOfSuper, "superString") + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.lang.String;\n" + + "import java.util.List;\n" + + "\n" + + "class Taco {\n" + + " List<?> extendsObject;\n" + + "\n" + + " List<? extends Serializable> extendsSerializable;\n" + + "\n" + + " List<? super String> superString;\n" + + "}\n"); + } + + @Test public void anonymousInnerClass() throws Exception { + ClassName foo = ClassName.get(tacosPackage, "Foo"); + ClassName bar = ClassName.get(tacosPackage, "Bar"); + ClassName thingThang = ClassName.get(tacosPackage, "Thing", "Thang"); + TypeName thingThangOfFooBar = ParameterizedTypeName.get(thingThang, foo, bar); + ClassName thung = ClassName.get(tacosPackage, "Thung"); + ClassName simpleThung = ClassName.get(tacosPackage, "SimpleThung"); + TypeName thungOfSuperBar = ParameterizedTypeName.get(thung, WildcardTypeName.supertypeOf(bar)); + TypeName thungOfSuperFoo = ParameterizedTypeName.get(thung, WildcardTypeName.supertypeOf(foo)); + TypeName simpleThungOfBar = ParameterizedTypeName.get(simpleThung, bar); + + ParameterSpec thungParameter = ParameterSpec.builder(thungOfSuperFoo, "thung") + .addModifiers(Modifier.FINAL) + .build(); + TypeSpec aSimpleThung = TypeSpec.anonymousClassBuilder(CodeBlock.of("$N", thungParameter)) + .superclass(simpleThungOfBar) + .addMethod(MethodSpec.methodBuilder("doSomething") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(bar, "bar") + .addCode("/* code snippets */\n") + .build()) + .build(); + TypeSpec aThingThang = TypeSpec.anonymousClassBuilder("") + .superclass(thingThangOfFooBar) + .addMethod(MethodSpec.methodBuilder("call") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(thungOfSuperBar) + .addParameter(thungParameter) + .addCode("return $L;\n", aSimpleThung) + .build()) + .build(); + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(FieldSpec.builder(thingThangOfFooBar, "NAME") + .addModifiers(Modifier.STATIC, Modifier.FINAL, Modifier.FINAL) + .initializer("$L", aThingThang) + .build()) + .build(); + + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "\n" + + "class Taco {\n" + + " static final Thing.Thang<Foo, Bar> NAME = new Thing.Thang<Foo, Bar>() {\n" + + " @Override\n" + + " public Thung<? super Bar> call(final Thung<? super Foo> thung) {\n" + + " return new SimpleThung<Bar>(thung) {\n" + + " @Override\n" + + " public void doSomething(Bar bar) {\n" + + " /* code snippets */\n" + + " }\n" + + " };\n" + + " }\n" + + " };\n" + + "}\n"); + } + + @Test public void annotatedParameters() throws Exception { + TypeSpec service = TypeSpec.classBuilder("Foo") + .addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(long.class, "id") + .addParameter(ParameterSpec.builder(String.class, "one") + .addAnnotation(ClassName.get(tacosPackage, "Ping")) + .build()) + .addParameter(ParameterSpec.builder(String.class, "two") + .addAnnotation(ClassName.get(tacosPackage, "Ping")) + .build()) + .addParameter(ParameterSpec.builder(String.class, "three") + .addAnnotation(AnnotationSpec.builder(ClassName.get(tacosPackage, "Pong")) + .addMember("value", "$S", "pong") + .build()) + .build()) + .addParameter(ParameterSpec.builder(String.class, "four") + .addAnnotation(ClassName.get(tacosPackage, "Ping")) + .build()) + .addCode("/* code snippets */\n") + .build()) + .build(); + + assertThat(toString(service)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Foo {\n" + + " public Foo(long id, @Ping String one, @Ping String two, @Pong(\"pong\") String three,\n" + + " @Ping String four) {\n" + + " /* code snippets */\n" + + " }\n" + + "}\n"); + } + + /** + * We had a bug where annotations were preventing us from doing the right thing when resolving + * imports. https://github.com/square/javapoet/issues/422 + */ + @Test public void annotationsAndJavaLangTypes() throws Exception { + ClassName freeRange = ClassName.get("javax.annotation", "FreeRange"); + TypeSpec taco = TypeSpec.classBuilder("EthicalTaco") + .addField(ClassName.get(String.class) + .annotated(AnnotationSpec.builder(freeRange).build()), "meat") + .build(); + + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "import javax.annotation.FreeRange;\n" + + "\n" + + "class EthicalTaco {\n" + + " @FreeRange String meat;\n" + + "}\n"); + } + + @Test public void retrofitStyleInterface() throws Exception { + ClassName observable = ClassName.get(tacosPackage, "Observable"); + ClassName fooBar = ClassName.get(tacosPackage, "FooBar"); + ClassName thing = ClassName.get(tacosPackage, "Thing"); + ClassName things = ClassName.get(tacosPackage, "Things"); + ClassName map = ClassName.get("java.util", "Map"); + ClassName string = ClassName.get("java.lang", "String"); + ClassName headers = ClassName.get(tacosPackage, "Headers"); + ClassName post = ClassName.get(tacosPackage, "POST"); + ClassName body = ClassName.get(tacosPackage, "Body"); + ClassName queryMap = ClassName.get(tacosPackage, "QueryMap"); + ClassName header = ClassName.get(tacosPackage, "Header"); + TypeSpec service = TypeSpec.interfaceBuilder("Service") + .addMethod(MethodSpec.methodBuilder("fooBar") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addAnnotation(AnnotationSpec.builder(headers) + .addMember("value", "$S", "Accept: application/json") + .addMember("value", "$S", "User-Agent: foobar") + .build()) + .addAnnotation(AnnotationSpec.builder(post) + .addMember("value", "$S", "/foo/bar") + .build()) + .returns(ParameterizedTypeName.get(observable, fooBar)) + .addParameter(ParameterSpec.builder(ParameterizedTypeName.get(things, thing), "things") + .addAnnotation(body) + .build()) + .addParameter(ParameterSpec.builder( + ParameterizedTypeName.get(map, string, string), "query") + .addAnnotation(AnnotationSpec.builder(queryMap) + .addMember("encodeValues", "false") + .build()) + .build()) + .addParameter(ParameterSpec.builder(string, "authorization") + .addAnnotation(AnnotationSpec.builder(header) + .addMember("value", "$S", "Authorization") + .build()) + .build()) + .build()) + .build(); + + assertThat(toString(service)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "import java.util.Map;\n" + + "\n" + + "interface Service {\n" + + " @Headers({\n" + + " \"Accept: application/json\",\n" + + " \"User-Agent: foobar\"\n" + + " })\n" + + " @POST(\"/foo/bar\")\n" + + " Observable<FooBar> fooBar(@Body Things<Thing> things,\n" + + " @QueryMap(encodeValues = false) Map<String, String> query,\n" + + " @Header(\"Authorization\") String authorization);\n" + + "}\n"); + } + + @Test public void annotatedField() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(FieldSpec.builder(String.class, "thing", Modifier.PRIVATE, Modifier.FINAL) + .addAnnotation(AnnotationSpec.builder(ClassName.get(tacosPackage, "JsonAdapter")) + .addMember("value", "$T.class", ClassName.get(tacosPackage, "Foo")) + .build()) + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " @JsonAdapter(Foo.class)\n" + + " private final String thing;\n" + + "}\n"); + } + + @Test public void annotatedClass() throws Exception { + ClassName someType = ClassName.get(tacosPackage, "SomeType"); + TypeSpec taco = TypeSpec.classBuilder("Foo") + .addAnnotation(AnnotationSpec.builder(ClassName.get(tacosPackage, "Something")) + .addMember("hi", "$T.$N", someType, "FIELD") + .addMember("hey", "$L", 12) + .addMember("hello", "$S", "goodbye") + .build()) + .addModifiers(Modifier.PUBLIC) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "@Something(\n" + + " hi = SomeType.FIELD,\n" + + " hey = 12,\n" + + " hello = \"goodbye\"\n" + + ")\n" + + "public class Foo {\n" + + "}\n"); + } + + @Test public void addAnnotationDisallowsNull() { + try { + TypeSpec.classBuilder("Foo").addAnnotation((AnnotationSpec) null); + fail(); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().isEqualTo("annotationSpec == null"); + } + try { + TypeSpec.classBuilder("Foo").addAnnotation((ClassName) null); + fail(); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().isEqualTo("type == null"); + } + try { + TypeSpec.classBuilder("Foo").addAnnotation((Class<?>) null); + fail(); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().isEqualTo("clazz == null"); + } + } + + @Test public void enumWithSubclassing() throws Exception { + TypeSpec roshambo = TypeSpec.enumBuilder("Roshambo") + .addModifiers(Modifier.PUBLIC) + .addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("") + .addJavadoc("Avalanche!\n") + .build()) + .addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat") + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addCode("return $S;\n", "paper airplane!") + .build()) + .build()) + .addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace sign") + .build()) + .addField(String.class, "handPosition", Modifier.PRIVATE, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(String.class, "handPosition") + .addCode("this.handPosition = handPosition;\n") + .build()) + .addMethod(MethodSpec.constructorBuilder() + .addCode("this($S);\n", "fist") + .build()) + .build(); + assertThat(toString(roshambo)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "public enum Roshambo {\n" + + " /**\n" + + " * Avalanche!\n" + + " */\n" + + " ROCK,\n" + + "\n" + + " PAPER(\"flat\") {\n" + + " @Override\n" + + " public String toString() {\n" + + " return \"paper airplane!\";\n" + + " }\n" + + " },\n" + + "\n" + + " SCISSORS(\"peace sign\");\n" + + "\n" + + " private final String handPosition;\n" + + "\n" + + " Roshambo(String handPosition) {\n" + + " this.handPosition = handPosition;\n" + + " }\n" + + "\n" + + " Roshambo() {\n" + + " this(\"fist\");\n" + + " }\n" + + "}\n"); + } + + /** https://github.com/square/javapoet/issues/193 */ + @Test public void enumsMayDefineAbstractMethods() throws Exception { + TypeSpec roshambo = TypeSpec.enumBuilder("Tortilla") + .addModifiers(Modifier.PUBLIC) + .addEnumConstant("CORN", TypeSpec.anonymousClassBuilder("") + .addMethod(MethodSpec.methodBuilder("fold") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .build()) + .build()) + .addMethod(MethodSpec.methodBuilder("fold") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .build()) + .build(); + assertThat(toString(roshambo)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "\n" + + "public enum Tortilla {\n" + + " CORN {\n" + + " @Override\n" + + " public void fold() {\n" + + " }\n" + + " };\n" + + "\n" + + " public abstract void fold();\n" + + "}\n"); + } + + @Test public void enumConstantsRequired() throws Exception { + try { + TypeSpec.enumBuilder("Roshambo") + .build(); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void onlyEnumsMayHaveEnumConstants() throws Exception { + try { + TypeSpec.classBuilder("Roshambo") + .addEnumConstant("ROCK") + .build(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void enumWithMembersButNoConstructorCall() throws Exception { + TypeSpec roshambo = TypeSpec.enumBuilder("Roshambo") + .addEnumConstant("SPOCK", TypeSpec.anonymousClassBuilder("") + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addCode("return $S;\n", "west side") + .build()) + .build()) + .build(); + assertThat(toString(roshambo)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "enum Roshambo {\n" + + " SPOCK {\n" + + " @Override\n" + + " public String toString() {\n" + + " return \"west side\";\n" + + " }\n" + + " }\n" + + "}\n"); + } + + /** https://github.com/square/javapoet/issues/253 */ + @Test public void enumWithAnnotatedValues() throws Exception { + TypeSpec roshambo = TypeSpec.enumBuilder("Roshambo") + .addModifiers(Modifier.PUBLIC) + .addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("") + .addAnnotation(Deprecated.class) + .build()) + .addEnumConstant("PAPER") + .addEnumConstant("SCISSORS") + .build(); + assertThat(toString(roshambo)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Deprecated;\n" + + "\n" + + "public enum Roshambo {\n" + + " @Deprecated\n" + + " ROCK,\n" + + "\n" + + " PAPER,\n" + + "\n" + + " SCISSORS\n" + + "}\n"); + } + + @Test public void methodThrows() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addModifiers(Modifier.ABSTRACT) + .addMethod(MethodSpec.methodBuilder("throwOne") + .addException(IOException.class) + .build()) + .addMethod(MethodSpec.methodBuilder("throwTwo") + .addException(IOException.class) + .addException(ClassName.get(tacosPackage, "SourCreamException")) + .build()) + .addMethod(MethodSpec.methodBuilder("abstractThrow") + .addModifiers(Modifier.ABSTRACT) + .addException(IOException.class) + .build()) + .addMethod(MethodSpec.methodBuilder("nativeThrow") + .addModifiers(Modifier.NATIVE) + .addException(IOException.class) + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.IOException;\n" + + "\n" + + "abstract class Taco {\n" + + " void throwOne() throws IOException {\n" + + " }\n" + + "\n" + + " void throwTwo() throws IOException, SourCreamException {\n" + + " }\n" + + "\n" + + " abstract void abstractThrow() throws IOException;\n" + + "\n" + + " native void nativeThrow() throws IOException;\n" + + "}\n"); + } + + @Test public void typeVariables() throws Exception { + TypeVariableName t = TypeVariableName.get("T"); + TypeVariableName p = TypeVariableName.get("P", Number.class); + ClassName location = ClassName.get(tacosPackage, "Location"); + TypeSpec typeSpec = TypeSpec.classBuilder("Location") + .addTypeVariable(t) + .addTypeVariable(p) + .addSuperinterface(ParameterizedTypeName.get(ClassName.get(Comparable.class), p)) + .addField(t, "label") + .addField(p, "x") + .addField(p, "y") + .addMethod(MethodSpec.methodBuilder("compareTo") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(int.class) + .addParameter(p, "p") + .addCode("return 0;\n") + .build()) + .addMethod(MethodSpec.methodBuilder("of") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addTypeVariable(t) + .addTypeVariable(p) + .returns(ParameterizedTypeName.get(location, t, p)) + .addParameter(t, "label") + .addParameter(p, "x") + .addParameter(p, "y") + .addCode("throw new $T($S);\n", UnsupportedOperationException.class, "TODO") + .build()) + .build(); + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Comparable;\n" + + "import java.lang.Number;\n" + + "import java.lang.Override;\n" + + "import java.lang.UnsupportedOperationException;\n" + + "\n" + + "class Location<T, P extends Number> implements Comparable<P> {\n" + + " T label;\n" + + "\n" + + " P x;\n" + + "\n" + + " P y;\n" + + "\n" + + " @Override\n" + + " public int compareTo(P p) {\n" + + " return 0;\n" + + " }\n" + + "\n" + + " public static <T, P extends Number> Location<T, P> of(T label, P x, P y) {\n" + + " throw new UnsupportedOperationException(\"TODO\");\n" + + " }\n" + + "}\n"); + } + + @Test public void typeVariableWithBounds() { + AnnotationSpec a = AnnotationSpec.builder(ClassName.get("com.squareup.tacos", "A")).build(); + TypeVariableName p = TypeVariableName.get("P", Number.class); + TypeVariableName q = (TypeVariableName) TypeVariableName.get("Q", Number.class).annotated(a); + TypeSpec typeSpec = TypeSpec.classBuilder("Location") + .addTypeVariable(p.withBounds(Comparable.class)) + .addTypeVariable(q.withBounds(Comparable.class)) + .addField(p, "x") + .addField(q, "y") + .build(); + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Comparable;\n" + + "import java.lang.Number;\n" + + "\n" + + "class Location<P extends Number & Comparable, @A Q extends Number & Comparable> {\n" + + " P x;\n" + + "\n" + + " @A Q y;\n" + + "}\n"); + } + + @Test public void classImplementsExtends() throws Exception { + ClassName taco = ClassName.get(tacosPackage, "Taco"); + ClassName food = ClassName.get("com.squareup.tacos", "Food"); + TypeSpec typeSpec = TypeSpec.classBuilder("Taco") + .addModifiers(Modifier.ABSTRACT) + .superclass(ParameterizedTypeName.get(ClassName.get(AbstractSet.class), food)) + .addSuperinterface(Serializable.class) + .addSuperinterface(ParameterizedTypeName.get(ClassName.get(Comparable.class), taco)) + .build(); + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.lang.Comparable;\n" + + "import java.util.AbstractSet;\n" + + "\n" + + "abstract class Taco extends AbstractSet<Food> " + + "implements Serializable, Comparable<Taco> {\n" + + "}\n"); + } + + @Test public void classImplementsNestedClass() throws Exception { + ClassName outer = ClassName.get(tacosPackage, "Outer"); + ClassName inner = outer.nestedClass("Inner"); + ClassName callable = ClassName.get(Callable.class); + TypeSpec typeSpec = TypeSpec.classBuilder("Outer") + .superclass(ParameterizedTypeName.get(callable, + inner)) + .addType(TypeSpec.classBuilder("Inner") + .addModifiers(Modifier.STATIC) + .build()) + .build(); + + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.util.concurrent.Callable;\n" + + "\n" + + "class Outer extends Callable<Outer.Inner> {\n" + + " static class Inner {\n" + + " }\n" + + "}\n"); + } + + @Test public void enumImplements() throws Exception { + TypeSpec typeSpec = TypeSpec.enumBuilder("Food") + .addSuperinterface(Serializable.class) + .addSuperinterface(Cloneable.class) + .addEnumConstant("LEAN_GROUND_BEEF") + .addEnumConstant("SHREDDED_CHEESE") + .build(); + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.lang.Cloneable;\n" + + "\n" + + "enum Food implements Serializable, Cloneable {\n" + + " LEAN_GROUND_BEEF,\n" + + "\n" + + " SHREDDED_CHEESE\n" + + "}\n"); + } + + @Test public void interfaceExtends() throws Exception { + ClassName taco = ClassName.get(tacosPackage, "Taco"); + TypeSpec typeSpec = TypeSpec.interfaceBuilder("Taco") + .addSuperinterface(Serializable.class) + .addSuperinterface(ParameterizedTypeName.get(ClassName.get(Comparable.class), taco)) + .build(); + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.lang.Comparable;\n" + + "\n" + + "interface Taco extends Serializable, Comparable<Taco> {\n" + + "}\n"); + } + + @Test public void nestedClasses() throws Exception { + ClassName taco = ClassName.get(tacosPackage, "Combo", "Taco"); + ClassName topping = ClassName.get(tacosPackage, "Combo", "Taco", "Topping"); + ClassName chips = ClassName.get(tacosPackage, "Combo", "Chips"); + ClassName sauce = ClassName.get(tacosPackage, "Combo", "Sauce"); + TypeSpec typeSpec = TypeSpec.classBuilder("Combo") + .addField(taco, "taco") + .addField(chips, "chips") + .addType(TypeSpec.classBuilder(taco.simpleName()) + .addModifiers(Modifier.STATIC) + .addField(ParameterizedTypeName.get(ClassName.get(List.class), topping), "toppings") + .addField(sauce, "sauce") + .addType(TypeSpec.enumBuilder(topping.simpleName()) + .addEnumConstant("SHREDDED_CHEESE") + .addEnumConstant("LEAN_GROUND_BEEF") + .build()) + .build()) + .addType(TypeSpec.classBuilder(chips.simpleName()) + .addModifiers(Modifier.STATIC) + .addField(topping, "topping") + .addField(sauce, "dippingSauce") + .build()) + .addType(TypeSpec.enumBuilder(sauce.simpleName()) + .addEnumConstant("SOUR_CREAM") + .addEnumConstant("SALSA") + .addEnumConstant("QUESO") + .addEnumConstant("MILD") + .addEnumConstant("FIRE") + .build()) + .build(); + + assertThat(toString(typeSpec)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.util.List;\n" + + "\n" + + "class Combo {\n" + + " Taco taco;\n" + + "\n" + + " Chips chips;\n" + + "\n" + + " static class Taco {\n" + + " List<Topping> toppings;\n" + + "\n" + + " Sauce sauce;\n" + + "\n" + + " enum Topping {\n" + + " SHREDDED_CHEESE,\n" + + "\n" + + " LEAN_GROUND_BEEF\n" + + " }\n" + + " }\n" + + "\n" + + " static class Chips {\n" + + " Taco.Topping topping;\n" + + "\n" + + " Sauce dippingSauce;\n" + + " }\n" + + "\n" + + " enum Sauce {\n" + + " SOUR_CREAM,\n" + + "\n" + + " SALSA,\n" + + "\n" + + " QUESO,\n" + + "\n" + + " MILD,\n" + + "\n" + + " FIRE\n" + + " }\n" + + "}\n"); + } + + @Test public void annotation() throws Exception { + TypeSpec annotation = TypeSpec.annotationBuilder("MyAnnotation") + .addModifiers(Modifier.PUBLIC) + .addMethod(MethodSpec.methodBuilder("test") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .defaultValue("$L", 0) + .returns(int.class) + .build()) + .build(); + + assertThat(toString(annotation)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "public @interface MyAnnotation {\n" + + " int test() default 0;\n" + + "}\n" + ); + } + + @Test public void innerAnnotationInAnnotationDeclaration() throws Exception { + TypeSpec bar = TypeSpec.annotationBuilder("Bar") + .addMethod(MethodSpec.methodBuilder("value") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .defaultValue("@$T", Deprecated.class) + .returns(Deprecated.class) + .build()) + .build(); + + assertThat(toString(bar)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Deprecated;\n" + + "\n" + + "@interface Bar {\n" + + " Deprecated value() default @Deprecated;\n" + + "}\n" + ); + } + + @Test public void annotationWithFields() { + FieldSpec field = FieldSpec.builder(int.class, "FOO") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("$L", 101) + .build(); + + TypeSpec anno = TypeSpec.annotationBuilder("Anno") + .addField(field) + .build(); + + assertThat(toString(anno)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "@interface Anno {\n" + + " int FOO = 101;\n" + + "}\n" + ); + } + + @Test + public void classCannotHaveDefaultValueForMethod() throws Exception { + try { + TypeSpec.classBuilder("Tacos") + .addMethod(MethodSpec.methodBuilder("test") + .addModifiers(Modifier.PUBLIC) + .defaultValue("0") + .returns(int.class) + .build()) + .build(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void classCannotHaveDefaultMethods() throws Exception { + try { + TypeSpec.classBuilder("Tacos") + .addMethod(MethodSpec.methodBuilder("test") + .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) + .returns(int.class) + .addCode(CodeBlock.builder().addStatement("return 0").build()) + .build()) + .build(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test + public void interfaceStaticMethods() throws Exception { + TypeSpec bar = TypeSpec.interfaceBuilder("Tacos") + .addMethod(MethodSpec.methodBuilder("test") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(int.class) + .addCode(CodeBlock.builder().addStatement("return 0").build()) + .build()) + .build(); + + assertThat(toString(bar)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "interface Tacos {\n" + + " static int test() {\n" + + " return 0;\n" + + " }\n" + + "}\n" + ); + } + + @Test + public void interfaceDefaultMethods() throws Exception { + TypeSpec bar = TypeSpec.interfaceBuilder("Tacos") + .addMethod(MethodSpec.methodBuilder("test") + .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) + .returns(int.class) + .addCode(CodeBlock.builder().addStatement("return 0").build()) + .build()) + .build(); + + assertThat(toString(bar)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "interface Tacos {\n" + + " default int test() {\n" + + " return 0;\n" + + " }\n" + + "}\n" + ); + } + + @Test public void referencedAndDeclaredSimpleNamesConflict() throws Exception { + FieldSpec internalTop = FieldSpec.builder( + ClassName.get(tacosPackage, "Top"), "internalTop").build(); + FieldSpec internalBottom = FieldSpec.builder( + ClassName.get(tacosPackage, "Top", "Middle", "Bottom"), "internalBottom").build(); + FieldSpec externalTop = FieldSpec.builder( + ClassName.get(donutsPackage, "Top"), "externalTop").build(); + FieldSpec externalBottom = FieldSpec.builder( + ClassName.get(donutsPackage, "Bottom"), "externalBottom").build(); + TypeSpec top = TypeSpec.classBuilder("Top") + .addField(internalTop) + .addField(internalBottom) + .addField(externalTop) + .addField(externalBottom) + .addType(TypeSpec.classBuilder("Middle") + .addField(internalTop) + .addField(internalBottom) + .addField(externalTop) + .addField(externalBottom) + .addType(TypeSpec.classBuilder("Bottom") + .addField(internalTop) + .addField(internalBottom) + .addField(externalTop) + .addField(externalBottom) + .build()) + .build()) + .build(); + assertThat(toString(top)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.squareup.donuts.Bottom;\n" + + "\n" + + "class Top {\n" + + " Top internalTop;\n" + + "\n" + + " Middle.Bottom internalBottom;\n" + + "\n" + + " com.squareup.donuts.Top externalTop;\n" + + "\n" + + " Bottom externalBottom;\n" + + "\n" + + " class Middle {\n" + + " Top internalTop;\n" + + "\n" + + " Bottom internalBottom;\n" + + "\n" + + " com.squareup.donuts.Top externalTop;\n" + + "\n" + + " com.squareup.donuts.Bottom externalBottom;\n" + + "\n" + + " class Bottom {\n" + + " Top internalTop;\n" + + "\n" + + " Bottom internalBottom;\n" + + "\n" + + " com.squareup.donuts.Top externalTop;\n" + + "\n" + + " com.squareup.donuts.Bottom externalBottom;\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void simpleNamesConflictInThisAndOtherPackage() throws Exception { + FieldSpec internalOther = FieldSpec.builder( + ClassName.get(tacosPackage, "Other"), "internalOther").build(); + FieldSpec externalOther = FieldSpec.builder( + ClassName.get(donutsPackage, "Other"), "externalOther").build(); + TypeSpec gen = TypeSpec.classBuilder("Gen") + .addField(internalOther) + .addField(externalOther) + .build(); + assertThat(toString(gen)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Gen {\n" + + " Other internalOther;\n" + + "\n" + + " com.squareup.donuts.Other externalOther;\n" + + "}\n"); + } + + @Test public void originatingElementsIncludesThoseOfNestedTypes() { + Element outerElement = Mockito.mock(Element.class); + Element innerElement = Mockito.mock(Element.class); + TypeSpec outer = TypeSpec.classBuilder("Outer") + .addOriginatingElement(outerElement) + .addType(TypeSpec.classBuilder("Inner") + .addOriginatingElement(innerElement) + .build()) + .build(); + assertThat(outer.originatingElements).containsExactly(outerElement, innerElement); + } + + @Test public void intersectionType() { + TypeVariableName typeVariable = TypeVariableName.get("T", Comparator.class, Serializable.class); + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("getComparator") + .addTypeVariable(typeVariable) + .returns(typeVariable) + .addCode("return null;\n") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.util.Comparator;\n" + + "\n" + + "class Taco {\n" + + " <T extends Comparator & Serializable> T getComparator() {\n" + + " return null;\n" + + " }\n" + + "}\n"); + } + + @Test public void arrayType() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(int[].class, "ints") + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " int[] ints;\n" + + "}\n"); + } + + @Test public void javadoc() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addJavadoc("A hard or soft tortilla, loosely folded and filled with whatever {@link \n") + .addJavadoc("{@link $T random} tex-mex stuff we could find in the pantry\n", Random.class) + .addJavadoc(CodeBlock.of("and some {@link $T} cheese.\n", String.class)) + .addField(FieldSpec.builder(boolean.class, "soft") + .addJavadoc("True for a soft flour tortilla; false for a crunchy corn tortilla.\n") + .build()) + .addMethod(MethodSpec.methodBuilder("refold") + .addJavadoc("Folds the back of this taco to reduce sauce leakage.\n" + + "\n" + + "<p>For {@link $T#KOREAN}, the front may also be folded.\n", Locale.class) + .addParameter(Locale.class, "locale") + .build()) + .build(); + // Mentioning a type in Javadoc will not cause an import to be added (java.util.Random here), + // but the short name will be used if it's already imported (java.util.Locale here). + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.util.Locale;\n" + + "\n" + + "/**\n" + + " * A hard or soft tortilla, loosely folded and filled with whatever {@link \n" + + " * {@link java.util.Random random} tex-mex stuff we could find in the pantry\n" + + " * and some {@link java.lang.String} cheese.\n" + + " */\n" + + "class Taco {\n" + + " /**\n" + + " * True for a soft flour tortilla; false for a crunchy corn tortilla.\n" + + " */\n" + + " boolean soft;\n" + + "\n" + + " /**\n" + + " * Folds the back of this taco to reduce sauce leakage.\n" + + " *\n" + + " * <p>For {@link Locale#KOREAN}, the front may also be folded.\n" + + " */\n" + + " void refold(Locale locale) {\n" + + " }\n" + + "}\n"); + } + + @Test public void annotationsInAnnotations() throws Exception { + ClassName beef = ClassName.get(tacosPackage, "Beef"); + ClassName chicken = ClassName.get(tacosPackage, "Chicken"); + ClassName option = ClassName.get(tacosPackage, "Option"); + ClassName mealDeal = ClassName.get(tacosPackage, "MealDeal"); + TypeSpec menu = TypeSpec.classBuilder("Menu") + .addAnnotation(AnnotationSpec.builder(mealDeal) + .addMember("price", "$L", 500) + .addMember("options", "$L", AnnotationSpec.builder(option) + .addMember("name", "$S", "taco") + .addMember("meat", "$T.class", beef) + .build()) + .addMember("options", "$L", AnnotationSpec.builder(option) + .addMember("name", "$S", "quesadilla") + .addMember("meat", "$T.class", chicken) + .build()) + .build()) + .build(); + assertThat(toString(menu)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "@MealDeal(\n" + + " price = 500,\n" + + " options = {\n" + + " @Option(name = \"taco\", meat = Beef.class),\n" + + " @Option(name = \"quesadilla\", meat = Chicken.class)\n" + + " }\n" + + ")\n" + + "class Menu {\n" + + "}\n"); + } + + @Test public void varargs() throws Exception { + TypeSpec taqueria = TypeSpec.classBuilder("Taqueria") + .addMethod(MethodSpec.methodBuilder("prepare") + .addParameter(int.class, "workers") + .addParameter(Runnable[].class, "jobs") + .varargs() + .build()) + .build(); + assertThat(toString(taqueria)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Runnable;\n" + + "\n" + + "class Taqueria {\n" + + " void prepare(int workers, Runnable... jobs) {\n" + + " }\n" + + "}\n"); + } + + @Test public void codeBlocks() throws Exception { + CodeBlock ifBlock = CodeBlock.builder() + .beginControlFlow("if (!a.equals(b))") + .addStatement("return i") + .endControlFlow() + .build(); + CodeBlock methodBody = CodeBlock.builder() + .addStatement("$T size = $T.min(listA.size(), listB.size())", int.class, Math.class) + .beginControlFlow("for ($T i = 0; i < size; i++)", int.class) + .addStatement("$T $N = $N.get(i)", String.class, "a", "listA") + .addStatement("$T $N = $N.get(i)", String.class, "b", "listB") + .add("$L", ifBlock) + .endControlFlow() + .addStatement("return size") + .build(); + CodeBlock fieldBlock = CodeBlock.builder() + .add("$>$>") + .add("\n$T.<$T, $T>builder()$>$>", ImmutableMap.class, String.class, String.class) + .add("\n.add($S, $S)", '\'', "'") + .add("\n.add($S, $S)", '&', "&") + .add("\n.add($S, $S)", '<', "<") + .add("\n.add($S, $S)", '>', ">") + .add("\n.build()$<$<") + .add("$<$<") + .build(); + FieldSpec escapeHtml = FieldSpec.builder(ParameterizedTypeName.get( + Map.class, String.class, String.class), "ESCAPE_HTML") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer(fieldBlock) + .build(); + TypeSpec util = TypeSpec.classBuilder("Util") + .addField(escapeHtml) + .addMethod(MethodSpec.methodBuilder("commonPrefixLength") + .returns(int.class) + .addParameter(ParameterizedTypeName.get(List.class, String.class), "listA") + .addParameter(ParameterizedTypeName.get(List.class, String.class), "listB") + .addCode(methodBody) + .build()) + .build(); + assertThat(toString(util)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import com.google.common.collect.ImmutableMap;\n" + + "import java.lang.Math;\n" + + "import java.lang.String;\n" + + "import java.util.List;\n" + + "import java.util.Map;\n" + + "\n" + + "class Util {\n" + + " private static final Map<String, String> ESCAPE_HTML = \n" + + " ImmutableMap.<String, String>builder()\n" + + " .add(\"\'\", \"'\")\n" + + " .add(\"&\", \"&\")\n" + + " .add(\"<\", \"<\")\n" + + " .add(\">\", \">\")\n" + + " .build();\n" + + "\n" + + " int commonPrefixLength(List<String> listA, List<String> listB) {\n" + + " int size = Math.min(listA.size(), listB.size());\n" + + " for (int i = 0; i < size; i++) {\n" + + " String a = listA.get(i);\n" + + " String b = listB.get(i);\n" + + " if (!a.equals(b)) {\n" + + " return i;\n" + + " }\n" + + " }\n" + + " return size;\n" + + " }\n" + + "}\n"); + } + + @Test public void indexedElseIf() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("choices") + .beginControlFlow("if ($1L != null || $1L == $2L)", "taco", "otherTaco") + .addStatement("$T.out.println($S)", System.class, "only one taco? NOO!") + .nextControlFlow("else if ($1L.$3L && $2L.$3L)", "taco", "otherTaco", "isSupreme()") + .addStatement("$T.out.println($S)", System.class, "taco heaven") + .endControlFlow() + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.System;\n" + + "\n" + + "class Taco {\n" + + " void choices() {\n" + + " if (taco != null || taco == otherTaco) {\n" + + " System.out.println(\"only one taco? NOO!\");\n" + + " } else if (taco.isSupreme() && otherTaco.isSupreme()) {\n" + + " System.out.println(\"taco heaven\");\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void elseIf() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("choices") + .beginControlFlow("if (5 < 4) ") + .addStatement("$T.out.println($S)", System.class, "wat") + .nextControlFlow("else if (5 < 6)") + .addStatement("$T.out.println($S)", System.class, "hello") + .endControlFlow() + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.System;\n" + + "\n" + + "class Taco {\n" + + " void choices() {\n" + + " if (5 < 4) {\n" + + " System.out.println(\"wat\");\n" + + " } else if (5 < 6) {\n" + + " System.out.println(\"hello\");\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void doWhile() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("loopForever") + .beginControlFlow("do") + .addStatement("$T.out.println($S)", System.class, "hello") + .endControlFlow("while (5 < 6)") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.System;\n" + + "\n" + + "class Taco {\n" + + " void loopForever() {\n" + + " do {\n" + + " System.out.println(\"hello\");\n" + + " } while (5 < 6);\n" + + " }\n" + + "}\n"); + } + + @Test public void inlineIndent() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("inlineIndent") + .addCode("if (3 < 4) {\n$>$T.out.println($S);\n$<}\n", System.class, "hello") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.System;\n" + + "\n" + + "class Taco {\n" + + " void inlineIndent() {\n" + + " if (3 < 4) {\n" + + " System.out.println(\"hello\");\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void defaultModifiersForInterfaceMembers() throws Exception { + TypeSpec taco = TypeSpec.interfaceBuilder("Taco") + .addField(FieldSpec.builder(String.class, "SHELL") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("$S", "crunchy corn") + .build()) + .addMethod(MethodSpec.methodBuilder("fold") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .build()) + .addType(TypeSpec.classBuilder("Topping") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "interface Taco {\n" + + " String SHELL = \"crunchy corn\";\n" + + "\n" + + " void fold();\n" + + "\n" + + " class Topping {\n" + + " }\n" + + "}\n"); + } + + @Test public void defaultModifiersForMemberInterfacesAndEnums() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addType(TypeSpec.classBuilder("Meat") + .addModifiers(Modifier.STATIC) + .build()) + .addType(TypeSpec.interfaceBuilder("Tortilla") + .addModifiers(Modifier.STATIC) + .build()) + .addType(TypeSpec.enumBuilder("Topping") + .addModifiers(Modifier.STATIC) + .addEnumConstant("SALSA") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " static class Meat {\n" + + " }\n" + + "\n" + + " interface Tortilla {\n" + + " }\n" + + "\n" + + " enum Topping {\n" + + " SALSA\n" + + " }\n" + + "}\n"); + } + + @Test public void membersOrdering() throws Exception { + // Hand out names in reverse-alphabetical order to defend against unexpected sorting. + TypeSpec taco = TypeSpec.classBuilder("Members") + .addType(TypeSpec.classBuilder("Z").build()) + .addType(TypeSpec.classBuilder("Y").build()) + .addField(String.class, "X", Modifier.STATIC) + .addField(String.class, "W") + .addField(String.class, "V", Modifier.STATIC) + .addField(String.class, "U") + .addMethod(MethodSpec.methodBuilder("T").addModifiers(Modifier.STATIC).build()) + .addMethod(MethodSpec.methodBuilder("S").build()) + .addMethod(MethodSpec.methodBuilder("R").addModifiers(Modifier.STATIC).build()) + .addMethod(MethodSpec.methodBuilder("Q").build()) + .addMethod(MethodSpec.constructorBuilder().addParameter(int.class, "p").build()) + .addMethod(MethodSpec.constructorBuilder().addParameter(long.class, "o").build()) + .build(); + // Static fields, instance fields, constructors, methods, classes. + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Members {\n" + + " static String X;\n" + + "\n" + + " static String V;\n" + + "\n" + + " String W;\n" + + "\n" + + " String U;\n" + + "\n" + + " Members(int p) {\n" + + " }\n" + + "\n" + + " Members(long o) {\n" + + " }\n" + + "\n" + + " static void T() {\n" + + " }\n" + + "\n" + + " void S() {\n" + + " }\n" + + "\n" + + " static void R() {\n" + + " }\n" + + "\n" + + " void Q() {\n" + + " }\n" + + "\n" + + " class Z {\n" + + " }\n" + + "\n" + + " class Y {\n" + + " }\n" + + "}\n"); + } + + @Test public void nativeMethods() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("nativeInt") + .addModifiers(Modifier.NATIVE) + .returns(int.class) + .build()) + // GWT JSNI + .addMethod(MethodSpec.methodBuilder("alert") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.NATIVE) + .addParameter(String.class, "msg") + .addCode(CodeBlock.builder() + .add(" /*-{\n") + .indent() + .addStatement("$$wnd.alert(msg)") + .unindent() + .add("}-*/") + .build()) + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " native int nativeInt();\n" + + "\n" + + " public static native void alert(String msg) /*-{\n" + + " $wnd.alert(msg);\n" + + " }-*/;\n" + + "}\n"); + } + + @Test public void nullStringLiteral() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(FieldSpec.builder(String.class, "NULL") + .initializer("$S", (Object) null) + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " String NULL = null;\n" + + "}\n"); + } + + @Test public void annotationToString() throws Exception { + AnnotationSpec annotation = AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "unused") + .build(); + assertThat(annotation.toString()).isEqualTo("@java.lang.SuppressWarnings(\"unused\")"); + } + + @Test public void codeBlockToString() throws Exception { + CodeBlock codeBlock = CodeBlock.builder() + .addStatement("$T $N = $S.substring(0, 3)", String.class, "s", "taco") + .build(); + assertThat(codeBlock.toString()).isEqualTo("java.lang.String s = \"taco\".substring(0, 3);\n"); + } + + @Test public void codeBlockAddStatementOfCodeBlockToString() throws Exception { + CodeBlock contents = CodeBlock.of("$T $N = $S.substring(0, 3)", String.class, "s", "taco"); + CodeBlock statement = CodeBlock.builder().addStatement(contents).build(); + assertThat(statement.toString()).isEqualTo("java.lang.String s = \"taco\".substring(0, 3);\n"); + } + + @Test public void fieldToString() throws Exception { + FieldSpec field = FieldSpec.builder(String.class, "s", Modifier.FINAL) + .initializer("$S.substring(0, 3)", "taco") + .build(); + assertThat(field.toString()) + .isEqualTo("final java.lang.String s = \"taco\".substring(0, 3);\n"); + } + + @Test public void methodToString() throws Exception { + MethodSpec method = MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addStatement("return $S", "taco") + .build(); + assertThat(method.toString()).isEqualTo("" + + "@java.lang.Override\n" + + "public java.lang.String toString() {\n" + + " return \"taco\";\n" + + "}\n"); + } + + @Test public void constructorToString() throws Exception { + MethodSpec constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ClassName.get(tacosPackage, "Taco"), "taco") + .addStatement("this.$N = $N", "taco", "taco") + .build(); + assertThat(constructor.toString()).isEqualTo("" + + "public Constructor(com.squareup.tacos.Taco taco) {\n" + + " this.taco = taco;\n" + + "}\n"); + } + + @Test public void parameterToString() throws Exception { + ParameterSpec parameter = ParameterSpec.builder(ClassName.get(tacosPackage, "Taco"), "taco") + .addModifiers(Modifier.FINAL) + .addAnnotation(ClassName.get("javax.annotation", "Nullable")) + .build(); + assertThat(parameter.toString()) + .isEqualTo("@javax.annotation.Nullable final com.squareup.tacos.Taco taco"); + } + + @Test public void classToString() throws Exception { + TypeSpec type = TypeSpec.classBuilder("Taco") + .build(); + assertThat(type.toString()).isEqualTo("" + + "class Taco {\n" + + "}\n"); + } + + @Test public void anonymousClassToString() throws Exception { + TypeSpec type = TypeSpec.anonymousClassBuilder("") + .addSuperinterface(Runnable.class) + .addMethod(MethodSpec.methodBuilder("run") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .build()) + .build(); + assertThat(type.toString()).isEqualTo("" + + "new java.lang.Runnable() {\n" + + " @java.lang.Override\n" + + " public void run() {\n" + + " }\n" + + "}"); + } + + @Test public void interfaceClassToString() throws Exception { + TypeSpec type = TypeSpec.interfaceBuilder("Taco") + .build(); + assertThat(type.toString()).isEqualTo("" + + "interface Taco {\n" + + "}\n"); + } + + @Test public void annotationDeclarationToString() throws Exception { + TypeSpec type = TypeSpec.annotationBuilder("Taco") + .build(); + assertThat(type.toString()).isEqualTo("" + + "@interface Taco {\n" + + "}\n"); + } + + private String toString(TypeSpec typeSpec) { + return JavaFile.builder(tacosPackage, typeSpec).build().toString(); + } + + @Test public void multilineStatement() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addStatement("return $S\n+ $S\n+ $S\n+ $S\n+ $S", + "Taco(", "beef,", "lettuce,", "cheese", ")") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " @Override\n" + + " public String toString() {\n" + + " return \"Taco(\"\n" + + " + \"beef,\"\n" + + " + \"lettuce,\"\n" + + " + \"cheese\"\n" + + " + \")\";\n" + + " }\n" + + "}\n"); + } + + @Test public void multilineStatementWithAnonymousClass() throws Exception { + TypeName stringComparator = ParameterizedTypeName.get(Comparator.class, String.class); + TypeName listOfString = ParameterizedTypeName.get(List.class, String.class); + TypeSpec prefixComparator = TypeSpec.anonymousClassBuilder("") + .addSuperinterface(stringComparator) + .addMethod(MethodSpec.methodBuilder("compare") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(int.class) + .addParameter(String.class, "a") + .addParameter(String.class, "b") + .addStatement("return a.substring(0, length)\n" + + ".compareTo(b.substring(0, length))") + .build()) + .build(); + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("comparePrefix") + .returns(stringComparator) + .addParameter(int.class, "length", Modifier.FINAL) + .addStatement("return $L", prefixComparator) + .build()) + .addMethod(MethodSpec.methodBuilder("sortPrefix") + .addParameter(listOfString, "list") + .addParameter(int.class, "length", Modifier.FINAL) + .addStatement("$T.sort(\nlist,\n$L)", Collections.class, prefixComparator) + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "import java.util.Collections;\n" + + "import java.util.Comparator;\n" + + "import java.util.List;\n" + + "\n" + + "class Taco {\n" + + " Comparator<String> comparePrefix(final int length) {\n" + + " return new Comparator<String>() {\n" + + " @Override\n" + + " public int compare(String a, String b) {\n" + + " return a.substring(0, length)\n" + + " .compareTo(b.substring(0, length));\n" + + " }\n" + + " };\n" + + " }\n" + + "\n" + + " void sortPrefix(List<String> list, final int length) {\n" + + " Collections.sort(\n" + + " list,\n" + + " new Comparator<String>() {\n" + + " @Override\n" + + " public int compare(String a, String b) {\n" + + " return a.substring(0, length)\n" + + " .compareTo(b.substring(0, length));\n" + + " }\n" + + " });\n" + + " }\n" + + "}\n"); + } + + @Test public void multilineStrings() throws Exception { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(FieldSpec.builder(String.class, "toppings") + .initializer("$S", "shell\nbeef\nlettuce\ncheese\n") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " String toppings = \"shell\\n\"\n" + + " + \"beef\\n\"\n" + + " + \"lettuce\\n\"\n" + + " + \"cheese\\n\";\n" + + "}\n"); + } + + @Test public void doubleFieldInitialization() { + try { + FieldSpec.builder(String.class, "listA") + .initializer("foo") + .initializer("bar") + .build(); + fail(); + } catch (IllegalStateException expected) { + } + + try { + FieldSpec.builder(String.class, "listA") + .initializer(CodeBlock.builder().add("foo").build()) + .initializer(CodeBlock.builder().add("bar").build()) + .build(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void nullAnnotationsAddition() { + try { + TypeSpec.classBuilder("Taco").addAnnotations(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("annotationSpecs == null"); + } + } + + @Test public void multipleAnnotationAddition() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addAnnotations(Arrays.asList( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "unchecked") + .build(), + AnnotationSpec.builder(Deprecated.class).build())) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Deprecated;\n" + + "import java.lang.SuppressWarnings;\n" + + "\n" + + "@SuppressWarnings(\"unchecked\")\n" + + "@Deprecated\n" + + "class Taco {\n" + + "}\n"); + } + + @Test public void nullFieldsAddition() { + try { + TypeSpec.classBuilder("Taco").addFields(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("fieldSpecs == null"); + } + } + + @Test public void multipleFieldAddition() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addFields(Arrays.asList( + FieldSpec.builder(int.class, "ANSWER", Modifier.STATIC, Modifier.FINAL).build(), + FieldSpec.builder(BigDecimal.class, "price", Modifier.PRIVATE).build())) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.math.BigDecimal;\n" + + "\n" + + "class Taco {\n" + + " static final int ANSWER;\n" + + "\n" + + " private BigDecimal price;\n" + + "}\n"); + } + + @Test public void nullMethodsAddition() { + try { + TypeSpec.classBuilder("Taco").addMethods(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("methodSpecs == null"); + } + } + + @Test public void multipleMethodAddition() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethods(Arrays.asList( + MethodSpec.methodBuilder("getAnswer") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(int.class) + .addStatement("return $L", 42) + .build(), + MethodSpec.methodBuilder("getRandomQuantity") + .addModifiers(Modifier.PUBLIC) + .returns(int.class) + .addJavadoc("chosen by fair dice roll ;)") + .addStatement("return $L", 4) + .build())) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " public static int getAnswer() {\n" + + " return 42;\n" + + " }\n" + + "\n" + + " /**\n" + + " * chosen by fair dice roll ;) */\n" + + " public int getRandomQuantity() {\n" + + " return 4;\n" + + " }\n" + + "}\n"); + } + + @Test public void nullSuperinterfacesAddition() { + try { + TypeSpec.classBuilder("Taco").addSuperinterfaces(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("superinterfaces == null"); + } + } + + @Test public void nullSingleSuperinterfaceAddition() { + try { + TypeSpec.classBuilder("Taco").addSuperinterface((TypeName) null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("superinterface == null"); + } + } + + @Test public void nullInSuperinterfaceIterableAddition() { + List<TypeName> superinterfaces = new ArrayList<>(); + superinterfaces.add(TypeName.get(List.class)); + superinterfaces.add(null); + + try { + TypeSpec.classBuilder("Taco").addSuperinterfaces(superinterfaces); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("superinterface == null"); + } + } + + @Test public void multipleSuperinterfaceAddition() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addSuperinterfaces(Arrays.asList( + TypeName.get(Serializable.class), + TypeName.get(EventListener.class))) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.util.EventListener;\n" + + "\n" + + "class Taco implements Serializable, EventListener {\n" + + "}\n"); + } + + @Test public void nullModifiersAddition() { + try { + TypeSpec.classBuilder("Taco").addModifiers((Modifier) null); + fail(); + } catch(IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("modifiers contain null"); + } + } + + @Test public void nullTypeVariablesAddition() { + try { + TypeSpec.classBuilder("Taco").addTypeVariables(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("typeVariables == null"); + } + } + + @Test public void multipleTypeVariableAddition() { + TypeSpec location = TypeSpec.classBuilder("Location") + .addTypeVariables(Arrays.asList( + TypeVariableName.get("T"), + TypeVariableName.get("P", Number.class))) + .build(); + assertThat(toString(location)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Number;\n" + + "\n" + + "class Location<T, P extends Number> {\n" + + "}\n"); + } + + @Test public void nullTypesAddition() { + try { + TypeSpec.classBuilder("Taco").addTypes(null); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected.getMessage()) + .isEqualTo("typeSpecs == null"); + } + } + + @Test public void multipleTypeAddition() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addTypes(Arrays.asList( + TypeSpec.classBuilder("Topping").build(), + TypeSpec.classBuilder("Sauce").build())) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " class Topping {\n" + + " }\n" + + "\n" + + " class Sauce {\n" + + " }\n" + + "}\n"); + } + + @Test public void tryCatch() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(MethodSpec.methodBuilder("addTopping") + .addParameter(ClassName.get("com.squareup.tacos", "Topping"), "topping") + .beginControlFlow("try") + .addCode("/* do something tricky with the topping */\n") + .nextControlFlow("catch ($T e)", + ClassName.get("com.squareup.tacos", "IllegalToppingException")) + .endControlFlow() + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " void addTopping(Topping topping) {\n" + + " try {\n" + + " /* do something tricky with the topping */\n" + + " } catch (IllegalToppingException e) {\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void ifElse() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod( + MethodSpec.methodBuilder("isDelicious") + .addParameter(TypeName.INT, "count") + .returns(TypeName.BOOLEAN) + .beginControlFlow("if (count > 0)") + .addStatement("return true") + .nextControlFlow("else") + .addStatement("return false") + .endControlFlow() + .build() + ) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " boolean isDelicious(int count) {\n" + + " if (count > 0) {\n" + + " return true;\n" + + " } else {\n" + + " return false;\n" + + " }\n" + + " }\n" + + "}\n"); + } + + @Test public void literalFromAnything() { + Object value = new Object() { + @Override public String toString() { + return "foo"; + } + }; + assertThat(CodeBlock.of("$L", value).toString()).isEqualTo("foo"); + } + + @Test public void nameFromCharSequence() { + assertThat(CodeBlock.of("$N", "text").toString()).isEqualTo("text"); + } + + @Test public void nameFromField() { + FieldSpec field = FieldSpec.builder(String.class, "field").build(); + assertThat(CodeBlock.of("$N", field).toString()).isEqualTo("field"); + } + + @Test public void nameFromParameter() { + ParameterSpec parameter = ParameterSpec.builder(String.class, "parameter").build(); + assertThat(CodeBlock.of("$N", parameter).toString()).isEqualTo("parameter"); + } + + @Test public void nameFromMethod() { + MethodSpec method = MethodSpec.methodBuilder("method") + .addModifiers(Modifier.ABSTRACT) + .returns(String.class) + .build(); + assertThat(CodeBlock.of("$N", method).toString()).isEqualTo("method"); + } + + @Test public void nameFromType() { + TypeSpec type = TypeSpec.classBuilder("Type").build(); + assertThat(CodeBlock.of("$N", type).toString()).isEqualTo("Type"); + } + + @Test public void nameFromUnsupportedType() { + try { + CodeBlock.builder().add("$N", String.class); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("expected name but was " + String.class); + } + } + + @Test public void stringFromAnything() { + Object value = new Object() { + @Override public String toString() { + return "foo"; + } + }; + assertThat(CodeBlock.of("$S", value).toString()).isEqualTo("\"foo\""); + } + + @Test public void stringFromNull() { + assertThat(CodeBlock.of("$S", new Object[] {null}).toString()).isEqualTo("null"); + } + + @Test public void typeFromTypeName() { + TypeName typeName = TypeName.get(String.class); + assertThat(CodeBlock.of("$T", typeName).toString()).isEqualTo("java.lang.String"); + } + + @Test public void typeFromTypeMirror() { + TypeMirror mirror = getElement(String.class).asType(); + assertThat(CodeBlock.of("$T", mirror).toString()).isEqualTo("java.lang.String"); + } + + @Test public void typeFromTypeElement() { + TypeElement element = getElement(String.class); + assertThat(CodeBlock.of("$T", element).toString()).isEqualTo("java.lang.String"); + } + + @Test public void typeFromReflectType() { + assertThat(CodeBlock.of("$T", String.class).toString()).isEqualTo("java.lang.String"); + } + + @Test public void typeFromUnsupportedType() { + try { + CodeBlock.builder().add("$T", "java.lang.String"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("expected type but was java.lang.String"); + } + } + + @Test public void tooFewArguments() { + try { + CodeBlock.builder().add("$S"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("index 1 for '$S' not in range (received 0 arguments)"); + } + } + + @Test public void unusedArgumentsRelative() { + try { + CodeBlock.builder().add("$L $L", "a", "b", "c"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("unused arguments: expected 2, received 3"); + } + } + + @Test public void unusedArgumentsIndexed() { + try { + CodeBlock.builder().add("$1L $2L", "a", "b", "c"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("unused argument: $3"); + } + try { + CodeBlock.builder().add("$1L $1L $1L", "a", "b", "c"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("unused arguments: $2, $3"); + } + try { + CodeBlock.builder().add("$3L $1L $3L $1L $3L", "a", "b", "c", "d"); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat().isEqualTo("unused arguments: $2, $4"); + } + } + + @Test public void superClassOnlyValidForClasses() { + try { + TypeSpec.annotationBuilder("A").superclass(ClassName.get(Object.class)); + fail(); + } catch (IllegalStateException expected) { + } + try { + TypeSpec.enumBuilder("E").superclass(ClassName.get(Object.class)); + fail(); + } catch (IllegalStateException expected) { + } + try { + TypeSpec.interfaceBuilder("I").superclass(ClassName.get(Object.class)); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void invalidSuperClass() { + try { + TypeSpec.classBuilder("foo") + .superclass(ClassName.get(List.class)) + .superclass(ClassName.get(Map.class)); + fail(); + } catch (IllegalStateException expected) { + } + try { + TypeSpec.classBuilder("foo") + .superclass(TypeName.INT); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void staticCodeBlock() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(String.class, "foo", Modifier.PRIVATE) + .addField(String.class, "FOO", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addStaticBlock(CodeBlock.builder() + .addStatement("FOO = $S", "FOO") + .build()) + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addCode("return FOO;\n") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " private static final String FOO;\n" + + "\n" + + " static {\n" + + " FOO = \"FOO\";\n" + + " }\n" + + "\n" + + " private String foo;\n" + + "\n" + + " @Override\n" + + " public String toString() {\n" + + " return FOO;\n" + + " }\n" + + "}\n"); + } + + @Test public void initializerBlockInRightPlace() { + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(String.class, "foo", Modifier.PRIVATE) + .addField(String.class, "FOO", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addStaticBlock(CodeBlock.builder() + .addStatement("FOO = $S", "FOO") + .build()) + .addMethod(MethodSpec.constructorBuilder().build()) + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addCode("return FOO;\n") + .build()) + .addInitializerBlock(CodeBlock.builder() + .addStatement("foo = $S", "FOO") + .build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " private static final String FOO;\n" + + "\n" + + " static {\n" + + " FOO = \"FOO\";\n" + + " }\n" + + "\n" + + " private String foo;\n" + + "\n" + + " {\n" + + " foo = \"FOO\";\n" + + " }\n" + + "\n" + + " Taco() {\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String toString() {\n" + + " return FOO;\n" + + " }\n" + + "}\n"); + } + + @Test public void initializersToBuilder() { + // Tests if toBuilder() contains correct static and instance initializers + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addField(String.class, "foo", Modifier.PRIVATE) + .addField(String.class, "FOO", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addStaticBlock(CodeBlock.builder() + .addStatement("FOO = $S", "FOO") + .build()) + .addMethod(MethodSpec.constructorBuilder().build()) + .addMethod(MethodSpec.methodBuilder("toString") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addCode("return FOO;\n") + .build()) + .addInitializerBlock(CodeBlock.builder() + .addStatement("foo = $S", "FOO") + .build()) + .build(); + + TypeSpec recreatedTaco = taco.toBuilder().build(); + assertThat(toString(taco)).isEqualTo(toString(recreatedTaco)); + + TypeSpec initializersAdded = taco.toBuilder() + .addInitializerBlock(CodeBlock.builder() + .addStatement("foo = $S", "instanceFoo") + .build()) + .addStaticBlock(CodeBlock.builder() + .addStatement("FOO = $S", "staticFoo") + .build()) + .build(); + + assertThat(toString(initializersAdded)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.Override;\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " private static final String FOO;\n" + + "\n" + + " static {\n" + + " FOO = \"FOO\";\n" + + " }\n" + + " static {\n" + + " FOO = \"staticFoo\";\n" + + " }\n" + + "\n" + + " private String foo;\n" + + "\n" + + " {\n" + + " foo = \"FOO\";\n" + + " }\n" + + " {\n" + + " foo = \"instanceFoo\";\n" + + " }\n" + + "\n" + + " Taco() {\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String toString() {\n" + + " return FOO;\n" + + " }\n" + + "}\n"); + } + + @Test public void initializerBlockUnsupportedExceptionOnInterface() { + TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder("Taco"); + try { + interfaceBuilder.addInitializerBlock(CodeBlock.builder().build()); + fail("Exception expected"); + } catch (UnsupportedOperationException e) { + } + } + + @Test public void initializerBlockUnsupportedExceptionOnAnnotation() { + TypeSpec.Builder annotationBuilder = TypeSpec.annotationBuilder("Taco"); + try { + annotationBuilder.addInitializerBlock(CodeBlock.builder().build()); + fail("Exception expected"); + } catch (UnsupportedOperationException e) { + } + } + + @Test public void lineWrapping() { + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("call"); + methodBuilder.addCode("$[call("); + for (int i = 0; i < 32; i++) { + methodBuilder.addParameter(String.class, "s" + i); + methodBuilder.addCode(i > 0 ? ",$W$S" : "$S", i); + } + methodBuilder.addCode(");$]\n"); + + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(methodBuilder.build()) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "import java.lang.String;\n" + + "\n" + + "class Taco {\n" + + " void call(String s0, String s1, String s2, String s3, String s4, String s5, String s6, String s7,\n" + + " String s8, String s9, String s10, String s11, String s12, String s13, String s14, String s15,\n" + + " String s16, String s17, String s18, String s19, String s20, String s21, String s22,\n" + + " String s23, String s24, String s25, String s26, String s27, String s28, String s29,\n" + + " String s30, String s31) {\n" + + " call(\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\", \"11\", \"12\", \"13\", \"14\", \"15\", \"16\",\n" + + " \"17\", \"18\", \"19\", \"20\", \"21\", \"22\", \"23\", \"24\", \"25\", \"26\", \"27\", \"28\", \"29\", \"30\", \"31\");\n" + + " }\n" + + "}\n"); + } + + @Test public void lineWrappingWithZeroWidthSpace() { + MethodSpec method = MethodSpec.methodBuilder("call") + .addCode("$[iAmSickOfWaitingInLine($Z") + .addCode("it, has, been, far, too, long, of, a, wait, and, i, would, like, to, eat, ") + .addCode("this, is, a, run, on, sentence") + .addCode(");$]\n") + .build(); + + TypeSpec taco = TypeSpec.classBuilder("Taco") + .addMethod(method) + .build(); + assertThat(toString(taco)).isEqualTo("" + + "package com.squareup.tacos;\n" + + "\n" + + "class Taco {\n" + + " void call() {\n" + + " iAmSickOfWaitingInLine(\n" + + " it, has, been, far, too, long, of, a, wait, and, i, would, like, to, eat, this, is, a, run, on, sentence);\n" + + " }\n" + + "}\n"); + } + + @Test public void equalsAndHashCode() { + TypeSpec a = TypeSpec.interfaceBuilder("taco").build(); + TypeSpec b = TypeSpec.interfaceBuilder("taco").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = TypeSpec.classBuilder("taco").build(); + b = TypeSpec.classBuilder("taco").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = TypeSpec.enumBuilder("taco").addEnumConstant("SALSA").build(); + b = TypeSpec.enumBuilder("taco").addEnumConstant("SALSA").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + a = TypeSpec.annotationBuilder("taco").build(); + b = TypeSpec.annotationBuilder("taco").build(); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test public void classNameFactories() { + ClassName className = ClassName.get("com.example", "Example"); + assertThat(TypeSpec.classBuilder(className).build().name).isEqualTo("Example"); + assertThat(TypeSpec.interfaceBuilder(className).build().name).isEqualTo("Example"); + assertThat(TypeSpec.enumBuilder(className).addEnumConstant("A").build().name).isEqualTo("Example"); + assertThat(TypeSpec.annotationBuilder(className).build().name).isEqualTo("Example"); + } +} diff --git a/src/test/java/com/squareup/javapoet/TypesEclipseTest.java b/src/test/java/com/squareup/javapoet/TypesEclipseTest.java new file mode 100644 index 0000000..2759f17 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/TypesEclipseTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2014 Google, 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 static com.google.common.base.Charsets.*; +import static com.google.common.base.Preconditions.*; + +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; + +import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.model.Statement; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +@RunWith(JUnit4.class) +public final class TypesEclipseTest extends AbstractTypesTest { + /** + * A {@link JUnit4} {@link Rule} that executes tests such that a instances of {@link Elements} and + * {@link Types} are available during execution. + * + * <p>To use this rule in a test, just add the following field: <pre> {@code + * @Rule public CompilationRule compilationRule = new CompilationRule();} + * + * @author Gregory Kick + */ + public static final class CompilationRule implements TestRule { + private Elements elements; + private Types types; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override public void evaluate() throws Throwable { + final AtomicReference<Throwable> thrown = new AtomicReference<>(); + boolean successful = compile(ImmutableList.of(new AbstractProcessor() { + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public Set<String> getSupportedAnnotationTypes() { + return ImmutableSet.of("*"); + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + elements = processingEnv.getElementUtils(); + types = processingEnv.getTypeUtils(); + } + + @Override + public boolean process(Set<? extends TypeElement> annotations, + RoundEnvironment roundEnv) { + // just run the test on the last round after compilation is over + if (roundEnv.processingOver()) { + try { + base.evaluate(); + } catch (Throwable e) { + thrown.set(e); + } + } + return false; + } + })); + checkState(successful); + Throwable t = thrown.get(); + if (t != null) { + throw t; + } + } + }; + } + + /** + * Returns the {@link Elements} instance associated with the current execution of the rule. + * + * @throws IllegalStateException if this method is invoked outside the execution of the rule. + */ + public Elements getElements() { + checkState(elements != null, "Not running within the rule"); + return elements; + } + + /** + * Returns the {@link Types} instance associated with the current execution of the rule. + * + * @throws IllegalStateException if this method is invoked outside the execution of the rule. + */ + public Types getTypes() { + checkState(elements != null, "Not running within the rule"); + return types; + } + + static private boolean compile(Iterable<? extends Processor> processors) { + JavaCompiler compiler = new EclipseCompiler(); + DiagnosticCollector<JavaFileObject> diagnosticCollector = + new DiagnosticCollector<>(); + JavaFileManager fileManager = compiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8); + JavaCompiler.CompilationTask task = compiler.getTask( + null, + fileManager, + diagnosticCollector, + ImmutableSet.of(), + ImmutableSet.of(TypesEclipseTest.class.getCanonicalName()), + ImmutableSet.of()); + task.setProcessors(processors); + return task.call(); + } + } + + @Rule public final CompilationRule compilation = new CompilationRule(); + + @Override + protected Elements getElements() { + return compilation.getElements(); + } + + @Override + protected Types getTypes() { + return compilation.getTypes(); + } +} diff --git a/src/test/java/com/squareup/javapoet/TypesTest.java b/src/test/java/com/squareup/javapoet/TypesTest.java new file mode 100644 index 0000000..2455ae5 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/TypesTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 Google, 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 javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import com.google.testing.compile.CompilationRule; + +@RunWith(JUnit4.class) +public final class TypesTest extends AbstractTypesTest { + @Rule public final CompilationRule compilation = new CompilationRule(); + + @Override + protected Elements getElements() { + return compilation.getElements(); + } + + @Override + protected Types getTypes() { + return compilation.getTypes(); + } +} diff --git a/src/test/java/com/squareup/javapoet/UtilTest.java b/src/test/java/com/squareup/javapoet/UtilTest.java new file mode 100644 index 0000000..d4b9c52 --- /dev/null +++ b/src/test/java/com/squareup/javapoet/UtilTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 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 static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class UtilTest { + @Test public void characterLiteral() { + assertEquals("a", Util.characterLiteralWithoutSingleQuotes('a')); + assertEquals("b", Util.characterLiteralWithoutSingleQuotes('b')); + assertEquals("c", Util.characterLiteralWithoutSingleQuotes('c')); + assertEquals("%", Util.characterLiteralWithoutSingleQuotes('%')); + // common escapes + assertEquals("\\b", Util.characterLiteralWithoutSingleQuotes('\b')); + assertEquals("\\t", Util.characterLiteralWithoutSingleQuotes('\t')); + assertEquals("\\n", Util.characterLiteralWithoutSingleQuotes('\n')); + assertEquals("\\f", Util.characterLiteralWithoutSingleQuotes('\f')); + assertEquals("\\r", Util.characterLiteralWithoutSingleQuotes('\r')); + assertEquals("\"", Util.characterLiteralWithoutSingleQuotes('"')); + assertEquals("\\'", Util.characterLiteralWithoutSingleQuotes('\'')); + assertEquals("\\\\", Util.characterLiteralWithoutSingleQuotes('\\')); + // octal escapes + assertEquals("\\u0000", Util.characterLiteralWithoutSingleQuotes('\0')); + assertEquals("\\u0007", Util.characterLiteralWithoutSingleQuotes('\7')); + assertEquals("?", Util.characterLiteralWithoutSingleQuotes('\77')); + assertEquals("\\u007f", Util.characterLiteralWithoutSingleQuotes('\177')); + assertEquals("¿", Util.characterLiteralWithoutSingleQuotes('\277')); + assertEquals("ÿ", Util.characterLiteralWithoutSingleQuotes('\377')); + // unicode escapes + assertEquals("\\u0000", Util.characterLiteralWithoutSingleQuotes('\u0000')); + assertEquals("\\u0001", Util.characterLiteralWithoutSingleQuotes('\u0001')); + assertEquals("\\u0002", Util.characterLiteralWithoutSingleQuotes('\u0002')); + assertEquals("€", Util.characterLiteralWithoutSingleQuotes('\u20AC')); + assertEquals("☃", Util.characterLiteralWithoutSingleQuotes('\u2603')); + assertEquals("♠", Util.characterLiteralWithoutSingleQuotes('\u2660')); + assertEquals("♣", Util.characterLiteralWithoutSingleQuotes('\u2663')); + assertEquals("♥", Util.characterLiteralWithoutSingleQuotes('\u2665')); + assertEquals("♦", Util.characterLiteralWithoutSingleQuotes('\u2666')); + assertEquals("✵", Util.characterLiteralWithoutSingleQuotes('\u2735')); + assertEquals("✺", Util.characterLiteralWithoutSingleQuotes('\u273A')); + assertEquals("/", Util.characterLiteralWithoutSingleQuotes('\uFF0F')); + } + + @Test public void stringLiteral() { + stringLiteral("abc"); + stringLiteral("♦♥♠♣"); + stringLiteral("€\\t@\\t$", "€\t@\t$", " "); + stringLiteral("abc();\\n\"\n + \"def();", "abc();\ndef();", " "); + stringLiteral("This is \\\"quoted\\\"!", "This is \"quoted\"!", " "); + stringLiteral("e^{i\\\\pi}+1=0", "e^{i\\pi}+1=0", " "); + } + + void stringLiteral(String string) { + stringLiteral(string, string, " "); + } + + void stringLiteral(String expected, String value, String indent) { + assertEquals("\"" + expected + "\"", Util.stringLiteralWithDoubleQuotes(value, indent)); + } +} |