├── .github ├── dependabot.yaml └── workflows │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logo.png ├── settings.gradle ├── timber-lint ├── build.gradle ├── gradle.properties └── src │ ├── main │ └── java │ │ └── timber │ │ └── lint │ │ ├── TimberIssueRegistry.kt │ │ └── WrongTimberUsageDetector.kt │ └── test │ └── java │ └── timber │ └── lint │ └── WrongTimberUsageDetectorTest.kt ├── timber-sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── timber │ │ ├── ExampleApp.java │ │ ├── FakeCrashLibrary.java │ │ └── ui │ │ ├── DemoActivity.java │ │ ├── JavaLintActivity.java │ │ └── KotlinLintActivity.kt │ └── res │ ├── layout │ └── demo_activity.xml │ └── values │ └── strings.xml └── timber ├── build.gradle ├── consumer-proguard-rules.pro ├── gradle.properties ├── japicmp └── build.gradle └── src ├── main ├── AndroidManifest.xml └── java │ └── timber │ └── log │ └── Timber.kt └── test └── java └── timber └── log ├── TimberJavaTest.java └── TimberTest.kt /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - '**' 8 | tags-ignore: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: gradle/actions/wrapper-validation@v4 18 | 19 | - uses: actions/setup-java@v4 20 | with: 21 | distribution: 'zulu' 22 | java-version: 11 23 | 24 | - run: ./gradlew build dokkaHtml 25 | 26 | - run: ./gradlew publish 27 | if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/timber' }} 28 | env: 29 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 30 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 31 | 32 | - name: Deploy docs to website 33 | if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/timber' }} 34 | uses: JamesIves/github-pages-deploy-action@releases/v3 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | BRANCH: site 38 | FOLDER: timber/build/dokka/html/ 39 | TARGET_FOLDER: docs/latest/ 40 | CLEAN: true 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-java@v4 16 | with: 17 | distribution: 'zulu' 18 | java-version: 11 19 | 20 | # TODO! 21 | # - run: ./gradlew -p mosaic publish 22 | # env: 23 | # ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 24 | # ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 25 | # ORG_GRADLE_PROJECT_signingKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 26 | 27 | - name: Extract release notes 28 | id: release_notes 29 | uses: ffurrer2/extract-release-notes@v2 30 | 31 | - name: Create release 32 | uses: softprops/action-gh-release@v2 33 | with: 34 | body: ${{ steps.release_notes.outputs.release_notes }} 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - run: ./gradlew dokkaHtml 39 | 40 | - name: Deploy docs to website 41 | uses: JamesIves/github-pages-deploy-action@releases/v3 42 | with: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | BRANCH: site 45 | FOLDER: timber/build/dokka/html/ 46 | TARGET_FOLDER: docs/5.x/ 47 | CLEAN: true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | eclipsebin 5 | 6 | bin 7 | gen 8 | build 9 | out 10 | lib 11 | 12 | .idea 13 | *.iml 14 | classes 15 | 16 | obj 17 | 18 | .DS_Store 19 | 20 | # Gradle 21 | .gradle 22 | jniLibs 23 | build 24 | local.properties 25 | reports 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [Unreleased] 4 | 5 | 6 | ## [5.0.1] - 2021-08-13 7 | 8 | ### Fixed 9 | 10 | - Fix TimberArgCount lint check false positive on some calls to `String.format`. 11 | 12 | 13 | ## [5.0.0] - 2021-08-10 14 | 15 | The library has been rewritten in Kotlin, but it remains binary-compatible with 4.x. 16 | The intent is to support Kotlin multiplatform in the future. 17 | This is otherwise a relatively minor, bug-fix release. 18 | 19 | ### Changed 20 | 21 | - Minimum supported API level is now 14. 22 | - Minimum supported AGP (for embedded lint checks) is now 7.0. 23 | 24 | ### Fixed 25 | 26 | - `DebugTree` now finds first non-library class name which prevents exceptions in optimized builds where expected stackframes may have been inlined. 27 | - Enforce 23-character truncated tag length until API 26 per AOSP sources. 28 | - Support `Long` type for date/time format arguments when validating format strings in lint checks. 29 | - Do not report string literal concatenation in lint checks on log message. 30 | 31 | 32 | ## [4.7.1] - 2018-06-28 33 | 34 | * Fix: Redundant argument lint check now works correctly on Kotlin sources. 35 | 36 | 37 | ## [4.7.0] - 2018-03-27 38 | 39 | * Fix: Support lint version 26.1.0. 40 | * Fix: Check single-argument log method in TimberExceptionLogging. 41 | 42 | 43 | ## [4.6.1] - 2018-02-12 44 | 45 | * Fix: Lint checks now handle more edge cases around exception and message source. 46 | * Fix: Useless `BuildConfig` class is no longer included. 47 | 48 | 49 | ## [4.6.0] - 2017-10-30 50 | 51 | * New: Lint checks have been ported to UAST, their stability improved, and quick-fix suggestions added. They require Android Gradle Plugin 3.0 or newer to run. 52 | * New: Added nullability annotations for Kotlin users. 53 | * Fix: Tag length truncation no longer occurs on API 24 or newer as the system no longer has a length restriction. 54 | * Fix: Handle when a `null` array is supplied for the message arguments. This can occur when using various bytecode optimization tools. 55 | 56 | 57 | ## [4.5.1] - 2017-01-20 58 | 59 | * Fix: String formatting lint check now correctly works with dates. 60 | 61 | 62 | ## [4.5.0] - 2017-01-09 63 | 64 | * New: Automatically truncate class name tags to Android's limit of 23 characters. 65 | * New: Lint check for detecting null/empty messages or using the exception message when logging an 66 | exception. Use the single-argument logging overloads instead. 67 | * Fix: Correct NPE in lint check when using String.format. 68 | 69 | 70 | ## [4.4.0] - 2016-12-06 71 | 72 | * New: `Tree.formatMessage` method allows customization of message formatting and rendering. 73 | * New: Lint checks ported to new IntelliJ PSI infrastructure. 74 | 75 | 76 | ## [4.3.1] - 2016-09-19 77 | 78 | * New: Add `isLoggable` convenience method which also provides the tag. 79 | 80 | 81 | ## [4.3.0] - 2016-08-18 82 | 83 | * New: Overloads for all log methods which accept only a `Throwable` without a message. 84 | 85 | 86 | ## [4.2.0] - 2016-08-12 87 | 88 | * New: `Timber.plant` now has a varargs overload for planting multiple trees at once. 89 | * New: minSdkVersion is now 9 because reasons. 90 | * Fix: Consume explicitly specified tag even if the message is determined as not loggable (due to level). 91 | * Fix: Allow lint checks to run when `Timber.tag(..).v(..)`-style logging is used. 92 | 93 | 94 | ## [4.1.2] - 2016-03-30 95 | 96 | * Fix: Tag-length lint check now only triggers on calls to `Timber`'s `tag` method. Previously it would 97 | match _any_ `tag` method and flag arguments longer than 23 characters. 98 | 99 | 100 | ## [4.1.1] - 2016-02-19 101 | 102 | * New: Add method for retreiving the number of installed trees. 103 | 104 | 105 | ## [4.1.0] - 2015-10-19 106 | 107 | * New: Consumer ProGuard rule automatically suppresses a warning for the use `@NonNls` on the 'message' 108 | argument for logging method. The warning was only for users running ProGuard and can safely be ignored. 109 | * New: Static `log` methods which accept a priority as a first argument makes dynamic logging at different 110 | levels easier to support. 111 | * Fix: Replace internal use of `Log.getStackTraceString` with our own implementation. This ensures that 112 | `UnknownHostException` errors are logged, which previously were suppressed. 113 | * Fix: 'BinaryOperationInTimber' lint rule now only triggers for string concatenation. 114 | 115 | 116 | ## [4.0.1] - 2015-10-07 117 | 118 | * Fix: TimberArgTypes lint rule now allows booleans and numbers in '%s' format markers. 119 | * Fix: Lint rules now support running on Java 7 VMs. 120 | 121 | 122 | ## [4.0.0] - 2015-10-07 123 | 124 | * New: Library is now an .aar! This means the lint rules are automatically applied to consuming 125 | projects. 126 | * New: `Tree.forest()` returns an immutable copy of all planted trees. 127 | * Fix: Ensure thread safety when logging and adding or removing trees concurrently. 128 | 129 | 130 | ## [3.1.0] - 2015-05-11 131 | 132 | * New: `Tree.isLoggable` method allows a tree to determine whether a statement should be logged 133 | based on its priority. Defaults to logging all levels. 134 | 135 | 136 | ## [3.0.2] - 2015-05-01 137 | 138 | * Fix: Strip multiple anonymous class markers (e.g., `$1$2`) from class names when `DebugTree` 139 | is creating an inferred tag. 140 | 141 | 142 | ## [3.0.1] - 2015-04-17 143 | 144 | * Fix: String formatting is now always applied when arguments are present. Previously it would 145 | only trigger when an exception was included. 146 | 147 | 148 | ## [3.0.0] - 2015-04-16 149 | 150 | * New: `Tree` and `DebugTree` APIs are much more extensible requiring only a single method to 151 | override. 152 | * New: `DebugTree` exposes `createStackElementTag` method for overriding to customize the 153 | reflection-based tag creation (for example, such as to add a line number). 154 | * WTF: Support for `wtf` log level. 155 | * `HollowTree` has been removed as it is no longer needed. Just extend `Tree`. 156 | * `TaggedTree` has been removed and its functionality folded into `Tree`. All `Tree` instances 157 | will receive any tags specified by a call to `tag`. 158 | * Fix: Multiple planted `DebugTree`s now each correctly received tags set from a call to `tag`. 159 | 160 | 161 | ## [2.7.1] - 2015-02-17 162 | 163 | * Fix: Switch method of getting calling class to be consistent across API levels. 164 | 165 | 166 | ## [2.7.0] - 2015-02-17 167 | 168 | * New: `DebugTree` subclasses can now override `logMessage` for access to the priority, tag, and 169 | entire message for every log. 170 | * Fix: Prevent overriding `Tree` and `TaggedTree` methods on `DebugTree`. 171 | 172 | 173 | ## [2.6.0] - 2015-02-17 174 | 175 | * New: `DebugTree` subclasses can now override `createTag()` to specify log tags. `nextTag()` is 176 | also accessible for querying if an explicit tag was set. 177 | 178 | 179 | ## [2.5.1] - 2015-01-19 180 | 181 | * Fix: Properly split lines which contain both newlines and are over 4000 characters. 182 | * Explicitly forbid `null` tree instances. 183 | 184 | 185 | ## [2.5.0] - 2014-11-08 186 | 187 | * New: `Timber.asTree()` exposes functionality as a `Tree` instance rather than static methods. 188 | 189 | 190 | ## [2.4.2] - 2014-11-07 191 | 192 | * Eliminate heap allocation when dispatching log calls. 193 | 194 | 195 | ## [2.4.1] - 2014-06-19 196 | 197 | * Fix: Calls with no message but a `Throwable` are now correctly logged. 198 | 199 | 200 | ## [2.4.0] - 2014-06-10 201 | 202 | * New: `uproot` and `uprootAll` methods allow removing trees. 203 | 204 | 205 | ## [2.3.0] - 2014-05-21 206 | 207 | * New: Messages longer than 4000 characters will be split into multiple lines. 208 | 209 | 210 | ## [2.2.2] - 2014-02-12 211 | 212 | * Fix: Include debug level in previous fix which avoids formatting messages with no arguments. 213 | 214 | 215 | ## [2.2.1] - 2014-02-11 216 | 217 | * Fix: Do not attempt to format log messages which do not have arguments. 218 | 219 | 220 | ## [2.2.0] - 2014-02-02 221 | 222 | * New: verbose log level added (`v()`). 223 | * New: `timber-lint` module adds lint check to ensure you are calling `Timber` and not `Log`. 224 | * Fix: Specifying custom tags is now thread-safe. 225 | 226 | 227 | ## [2.1.0] - 2013-11-21 228 | 229 | * New: `tag` method allows specifying custom one-time tag. Redux! 230 | 231 | 232 | ## [2.0.0] - 2013-10-21 233 | 234 | * Logging API is now exposed as static methods on `Timber`. Behavior is added by installing `Tree` 235 | instances for logging. 236 | 237 | 238 | ## [1.1.0] - 2013-07-22 239 | 240 | * New: `tag` method allows specifying custom one-time tag. 241 | * Fix: Exception-containing methods now log at the correct level. 242 | 243 | 244 | ## [1.0.0] - 2013-07-17 245 | 246 | Initial cut. (Get it?) 247 | 248 | 249 | 250 | 251 | [Unreleased]: https://github.com/JakeWharton/timber/compare/5.0.1...HEAD 252 | [5.0.1]: https://github.com/JakeWharton/timber/releases/tag/5.0.1 253 | [5.0.0]: https://github.com/JakeWharton/timber/releases/tag/5.0.0 254 | [4.7.1]: https://github.com/JakeWharton/timber/releases/tag/4.7.1 255 | [4.7.0]: https://github.com/JakeWharton/timber/releases/tag/4.7.0 256 | [4.6.1]: https://github.com/JakeWharton/timber/releases/tag/4.6.1 257 | [4.6.0]: https://github.com/JakeWharton/timber/releases/tag/4.6.0 258 | [4.5.1]: https://github.com/JakeWharton/timber/releases/tag/4.5.1 259 | [4.5.0]: https://github.com/JakeWharton/timber/releases/tag/4.5.0 260 | [4.4.0]: https://github.com/JakeWharton/timber/releases/tag/4.4.0 261 | [4.3.1]: https://github.com/JakeWharton/timber/releases/tag/4.3.1 262 | [4.3.0]: https://github.com/JakeWharton/timber/releases/tag/4.3.0 263 | [4.2.0]: https://github.com/JakeWharton/timber/releases/tag/4.2.0 264 | [4.1.2]: https://github.com/JakeWharton/timber/releases/tag/4.1.2 265 | [4.1.1]: https://github.com/JakeWharton/timber/releases/tag/4.1.1 266 | [4.1.0]: https://github.com/JakeWharton/timber/releases/tag/4.1.0 267 | [4.0.1]: https://github.com/JakeWharton/timber/releases/tag/4.0.1 268 | [4.0.0]: https://github.com/JakeWharton/timber/releases/tag/4.0.0 269 | [3.1.0]: https://github.com/JakeWharton/timber/releases/tag/3.1.0 270 | [3.0.2]: https://github.com/JakeWharton/timber/releases/tag/3.0.2 271 | [3.0.1]: https://github.com/JakeWharton/timber/releases/tag/3.0.1 272 | [3.0.0]: https://github.com/JakeWharton/timber/releases/tag/3.0.0 273 | [2.7.1]: https://github.com/JakeWharton/timber/releases/tag/2.7.1 274 | [2.7.0]: https://github.com/JakeWharton/timber/releases/tag/2.7.0 275 | [2.6.0]: https://github.com/JakeWharton/timber/releases/tag/2.6.0 276 | [2.5.1]: https://github.com/JakeWharton/timber/releases/tag/2.5.1 277 | [2.5.0]: https://github.com/JakeWharton/timber/releases/tag/2.5.0 278 | [2.4.2]: https://github.com/JakeWharton/timber/releases/tag/2.4.2 279 | [2.4.1]: https://github.com/JakeWharton/timber/releases/tag/2.4.1 280 | [2.4.0]: https://github.com/JakeWharton/timber/releases/tag/2.4.0 281 | [2.3.0]: https://github.com/JakeWharton/timber/releases/tag/2.3.0 282 | [2.2.2]: https://github.com/JakeWharton/timber/releases/tag/2.2.2 283 | [2.2.1]: https://github.com/JakeWharton/timber/releases/tag/2.2.1 284 | [2.2.0]: https://github.com/JakeWharton/timber/releases/tag/2.2.0 285 | [2.1.0]: https://github.com/JakeWharton/timber/releases/tag/2.1.0 286 | [2.0.0]: https://github.com/JakeWharton/timber/releases/tag/2.0.0 287 | [1.1.0]: https://github.com/JakeWharton/timber/releases/tag/1.1.0 288 | [1.0.0]: https://github.com/JakeWharton/timber/releases/tag/1.0.0 289 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Timber](logo.png) 2 | 3 | This is a logger with a small, extensible API which provides utility on top of Android's normal 4 | `Log` class. 5 | 6 | I copy this class into all the little apps I make. I'm tired of doing it. Now it's a library. 7 | 8 | Behavior is added through `Tree` instances. You can install an instance by calling `Timber.plant`. 9 | Installation of `Tree`s should be done as early as possible. The `onCreate` of your application is 10 | the most logical choice. 11 | 12 | The `DebugTree` implementation will automatically figure out from which class it's being called and 13 | use that class name as its tag. Since the tags vary, it works really well when coupled with a log 14 | reader like [Pidcat][1]. 15 | 16 | There are no `Tree` implementations installed by default because every time you log in production, a 17 | puppy dies. 18 | 19 | 20 | Usage 21 | ----- 22 | 23 | Two easy steps: 24 | 25 | 1. Install any `Tree` instances you want in the `onCreate` of your application class. 26 | 2. Call `Timber`'s static methods everywhere throughout your app. 27 | 28 | Check out the sample app in `timber-sample/` to see it in action. 29 | 30 | 31 | Lint 32 | ---- 33 | 34 | Timber ships with embedded lint rules to detect problems in your app. 35 | 36 | * **TimberArgCount** (Error) - Detects an incorrect number of arguments passed to a `Timber` call for 37 | the specified format string. 38 | 39 | Example.java:35: Error: Wrong argument count, format string Hello %s %s! requires 2 but format call supplies 1 [TimberArgCount] 40 | Timber.d("Hello %s %s!", firstName); 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | 43 | * **TimberArgTypes** (Error) - Detects arguments which are of the wrong type for the specified format string. 44 | 45 | Example.java:35: Error: Wrong argument type for formatting argument '#0' in success = %b: conversion is 'b', received String (argument #2 in method call) [TimberArgTypes] 46 | Timber.d("success = %b", taskName); 47 | ~~~~~~~~ 48 | * **TimberTagLength** (Error) - Detects the use of tags which are longer than Android's maximum length of 23. 49 | 50 | Example.java:35: Error: The logging tag can be at most 23 characters, was 35 (TagNameThatIsReallyReallyReallyLong) [TimberTagLength] 51 | Timber.tag("TagNameThatIsReallyReallyReallyLong").d("Hello %s %s!", firstName, lastName); 52 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | 54 | * **LogNotTimber** (Warning) - Detects usages of Android's `Log` that should be using `Timber`. 55 | 56 | Example.java:35: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 57 | Log.d("Greeting", "Hello " + firstName + " " + lastName + "!"); 58 | ~ 59 | 60 | * **StringFormatInTimber** (Warning) - Detects `String.format` used inside of a `Timber` call. Timber 61 | handles string formatting automatically. 62 | 63 | Example.java:35: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 64 | Timber.d(String.format("Hello, %s %s", firstName, lastName)); 65 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 66 | 67 | * **BinaryOperationInTimber** (Warning) - Detects string concatenation inside of a `Timber` call. Timber 68 | handles string formatting automatically and should be preferred over manual concatenation. 69 | 70 | Example.java:35: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber] 71 | Timber.d("Hello " + firstName + " " + lastName + "!"); 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | 74 | * **TimberExceptionLogging** (Warning) - Detects the use of null or empty messages, or using the exception message 75 | when logging an exception. 76 | 77 | Example.java:35: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging] 78 | Timber.d(e, e.getMessage()); 79 | ~~~~~~~~~~~~~~ 80 | 81 | 82 | Download 83 | -------- 84 | 85 | ```groovy 86 | repositories { 87 | mavenCentral() 88 | } 89 | 90 | dependencies { 91 | implementation 'com.jakewharton.timber:timber:5.0.1' 92 | } 93 | ``` 94 | 95 | Documentation is available at [jakewharton.github.io/timber/docs/5.x/](https://jakewharton.github.io/timber/docs/5.x/). 96 | 97 |
98 | Snapshots of the development version are available in Sonatype's snapshots repository. 99 |

100 | 101 | ```groovy 102 | repositories { 103 | mavenCentral() 104 | maven { 105 | url 'https://oss.sonatype.org/content/repositories/snapshots/' 106 | } 107 | } 108 | 109 | dependencies { 110 | implementation 'com.jakewharton.timber:timber:5.1.0-SNAPSHOT' 111 | } 112 | ``` 113 | 114 | Snapshot documentation is available at [jakewharton.github.io/timber/docs/latest/](https://jakewharton.github.io/timber/docs/latest/). 115 | 116 |

117 |
118 | 119 | 120 | License 121 | ------- 122 | 123 | Copyright 2013 Jake Wharton 124 | 125 | Licensed under the Apache License, Version 2.0 (the "License"); 126 | you may not use this file except in compliance with the License. 127 | You may obtain a copy of the License at 128 | 129 | http://www.apache.org/licenses/LICENSE-2.0 130 | 131 | Unless required by applicable law or agreed to in writing, software 132 | distributed under the License is distributed on an "AS IS" BASIS, 133 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 134 | See the License for the specific language governing permissions and 135 | limitations under the License. 136 | 137 | 138 | 139 | [1]: http://github.com/JakeWharton/pidcat/ 140 | [snap]: https://oss.sonatype.org/content/repositories/snapshots/ 141 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update the `VERSION_NAME` in `gradle.properties` to the release version. 4 | 5 | 2. Update the `CHANGELOG.md`: 6 | 1. Change the `Unreleased` header to the release version. 7 | 2. Add a link URL to ensure the header link works. 8 | 3. Add a new `Unreleased` section to the top. 9 | 10 | 3. Update the `README.md`: 11 | 1. Change the "Download" section to reflect the new release version. 12 | 2. Change the snapshot section to reflect the next "SNAPSHOT" version, if it is changing. 13 | 3. Update the Kotlin version compatibility table 14 | 15 | 4. Commit 16 | 17 | ``` 18 | $ git commit -am "Prepare version X.Y.X" 19 | ``` 20 | 21 | 5. Manually release and upload artifacts 22 | 1. Run `./gradlew clean publish` 23 | 2. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 24 | 3. If either fails, drop the Sonatype repo, fix the problem, commit, and restart this section. 25 | 26 | 6. Tag 27 | 28 | ``` 29 | $ git tag -am "Version X.Y.Z" X.Y.Z 30 | ``` 31 | 32 | 7. Update the `VERSION_NAME` in `gradle.properties` to the next "SNAPSHOT" version. 33 | 34 | 8. Commit 35 | 36 | ``` 37 | $ git commit -am "Prepare next development version" 38 | ``` 39 | 40 | 9. Push! 41 | 42 | ``` 43 | $ git push && git push --tags 44 | ``` 45 | 46 | This will trigger a GitHub Action workflow which will create a GitHub release. 47 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.github.ben-manes.versions' 2 | 3 | buildscript { 4 | ext.versions = [ 5 | 'minSdk': 14, 6 | 'compileSdk': 30, 7 | 8 | 'kotlin': '1.5.21', 9 | 'autoService': '1.0-rc7', 10 | 11 | // Update WrongTimberUsageDetectorTest#innerStringFormatWithStaticImport when >= 7.1.0-alpha07 12 | 'androidPlugin': '7.0.0', 13 | 'androidTools': '30.0.0', 14 | ] 15 | 16 | ext.deps = [ 17 | androidPlugin: "com.android.tools.build:gradle:${versions.androidPlugin}", 18 | 'kotlin': [ 19 | 'plugin': "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", 20 | 'stdlib': "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}", 21 | ], 22 | 'lint': [ 23 | 'core': "com.android.tools.lint:lint:${versions.androidTools}", 24 | 'api': "com.android.tools.lint:lint-api:${versions.androidTools}", 25 | 'checks': "com.android.tools.lint:lint-checks:${versions.androidTools}", 26 | 'tests': "com.android.tools.lint:lint-tests:${versions.androidTools}", 27 | ], 28 | 'auto': [ 29 | 'service': "com.google.auto.service:auto-service:${versions.autoService}", 30 | 'serviceAnnotations': "com.google.auto.service:auto-service-annotations:${versions.autoService}", 31 | ], 32 | annotations: 'org.jetbrains:annotations:20.1.0', 33 | 34 | junit: 'junit:junit:4.13.2', 35 | truth: 'com.google.truth:truth:1.1.2', 36 | robolectric: 'org.robolectric:robolectric:4.6.1', 37 | ] 38 | 39 | repositories { 40 | mavenCentral() 41 | google() 42 | gradlePluginPortal() 43 | } 44 | 45 | dependencies { 46 | classpath deps.androidPlugin 47 | classpath deps.kotlin.plugin 48 | classpath 'com.github.ben-manes:gradle-versions-plugin:0.39.0' 49 | classpath 'me.champeau.gradle:japicmp-gradle-plugin:0.2.9' 50 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.14.2' 51 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.32' 52 | } 53 | } 54 | 55 | subprojects { 56 | repositories { 57 | mavenCentral() 58 | google() 59 | } 60 | 61 | tasks.withType(Test) { 62 | testLogging { 63 | events "failed" 64 | exceptionFormat "full" 65 | showExceptions true 66 | showStackTraces true 67 | showCauses true 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.jakewharton.timber 2 | 3 | # HEY! If you change the major version here be sure to update release.yaml doc target folder! 4 | VERSION_NAME=5.1.0-SNAPSHOT 5 | 6 | POM_DESCRIPTION=No-nonsense injectable logging. 7 | 8 | POM_URL=https://github.com/JakeWharton/timber 9 | POM_SCM_URL=https://github.com/JakeWharton/timber 10 | POM_SCM_CONNECTION=scm:git:git://github.com/JakeWharton/timber.git 11 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/JakeWharton/timber.git 12 | 13 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 14 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 15 | POM_LICENCE_DIST=repo 16 | 17 | POM_DEVELOPER_ID=jakewharton 18 | POM_DEVELOPER_NAME=Jake Wharton 19 | 20 | org.gradle.jvmargs=-Xmx1536M 21 | 22 | android.useAndroidX=true 23 | android.enableJetifier=false 24 | 25 | android.defaults.buildfeatures.buildconfig=false 26 | android.defaults.buildfeatures.aidl=false 27 | android.defaults.buildfeatures.renderscript=false 28 | android.defaults.buildfeatures.resvalues=false 29 | android.defaults.buildfeatures.shaders=false 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeWharton/timber/beb8051248164a74b264d30427f633aaf4bda841/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeWharton/timber/beb8051248164a74b264d30427f633aaf4bda841/logo.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':timber' 2 | include ':timber:japicmp' 3 | include ':timber-lint' 4 | include ':timber-sample' 5 | 6 | rootProject.name = 'timber-root' 7 | -------------------------------------------------------------------------------- /timber-lint/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'org.jetbrains.kotlin.kapt' 4 | apply plugin: 'com.android.lint' 5 | 6 | targetCompatibility = JavaVersion.VERSION_1_8 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | 9 | dependencies { 10 | compileOnly deps.lint.api 11 | compileOnly deps.lint.checks 12 | compileOnly deps.auto.serviceAnnotations 13 | kapt deps.auto.service 14 | testImplementation deps.junit 15 | testImplementation deps.lint.core 16 | testImplementation deps.lint.tests 17 | testImplementation deps.junit 18 | } 19 | -------------------------------------------------------------------------------- /timber-lint/gradle.properties: -------------------------------------------------------------------------------- 1 | # needed so that :timber:prepareLintJarForPublish can succeed 2 | # Remove when the bug described in https://issuetracker.google.com/issues/161727305 is fixed 3 | kotlin.stdlib.default.dependency=false 4 | -------------------------------------------------------------------------------- /timber-lint/src/main/java/timber/lint/TimberIssueRegistry.kt: -------------------------------------------------------------------------------- 1 | package timber.lint 2 | 3 | import com.android.tools.lint.client.api.IssueRegistry 4 | import com.android.tools.lint.client.api.Vendor 5 | import com.android.tools.lint.detector.api.CURRENT_API 6 | import com.android.tools.lint.detector.api.Issue 7 | import com.google.auto.service.AutoService 8 | 9 | @Suppress("UnstableApiUsage", "unused") 10 | @AutoService(value = [IssueRegistry::class]) 11 | class TimberIssueRegistry : IssueRegistry() { 12 | override val issues: List 13 | get() = WrongTimberUsageDetector.issues.asList() 14 | 15 | override val api: Int 16 | get() = CURRENT_API 17 | 18 | /** 19 | * works with Studio 4.0 or later; see 20 | * [com.android.tools.lint.detector.api.describeApi] 21 | */ 22 | override val minApi: Int 23 | get() = 7 24 | 25 | override val vendor = Vendor( 26 | vendorName = "JakeWharton/timber", 27 | identifier = "com.jakewharton.timber:timber:{version}", 28 | feedbackUrl = "https://github.com/JakeWharton/timber/issues", 29 | ) 30 | } -------------------------------------------------------------------------------- /timber-lint/src/main/java/timber/lint/WrongTimberUsageDetector.kt: -------------------------------------------------------------------------------- 1 | package timber.lint 2 | 3 | import com.android.tools.lint.detector.api.skipParentheses 4 | import org.jetbrains.uast.util.isMethodCall 5 | import com.android.tools.lint.detector.api.minSdkLessThan 6 | import com.android.tools.lint.detector.api.isString 7 | import com.android.tools.lint.detector.api.isKotlin 8 | import org.jetbrains.uast.isInjectionHost 9 | import org.jetbrains.uast.evaluateString 10 | import com.android.tools.lint.detector.api.Detector 11 | import com.android.tools.lint.detector.api.JavaContext 12 | import org.jetbrains.uast.UCallExpression 13 | import com.intellij.psi.PsiMethod 14 | import com.android.tools.lint.client.api.JavaEvaluator 15 | import com.android.tools.lint.detector.api.LintFix 16 | import org.jetbrains.uast.UElement 17 | import org.jetbrains.uast.UMethod 18 | import org.jetbrains.uast.UExpression 19 | import com.android.tools.lint.detector.api.Incident 20 | import org.jetbrains.uast.UQualifiedReferenceExpression 21 | import org.jetbrains.uast.UBinaryExpression 22 | import org.jetbrains.uast.UastBinaryOperator 23 | import org.jetbrains.uast.UIfExpression 24 | import com.intellij.psi.PsiMethodCallExpression 25 | import com.intellij.psi.PsiLiteralExpression 26 | import com.intellij.psi.PsiType 27 | import com.intellij.psi.PsiClassType 28 | import com.android.tools.lint.checks.StringFormatDetector 29 | import com.android.tools.lint.client.api.TYPE_BOOLEAN 30 | import com.android.tools.lint.client.api.TYPE_BOOLEAN_WRAPPER 31 | import com.android.tools.lint.client.api.TYPE_BYTE 32 | import com.android.tools.lint.client.api.TYPE_BYTE_WRAPPER 33 | import com.android.tools.lint.client.api.TYPE_CHAR 34 | import com.android.tools.lint.client.api.TYPE_DOUBLE 35 | import com.android.tools.lint.client.api.TYPE_DOUBLE_WRAPPER 36 | import com.android.tools.lint.client.api.TYPE_FLOAT 37 | import com.android.tools.lint.client.api.TYPE_FLOAT_WRAPPER 38 | import com.android.tools.lint.client.api.TYPE_INT 39 | import com.android.tools.lint.client.api.TYPE_INTEGER_WRAPPER 40 | import com.android.tools.lint.client.api.TYPE_LONG 41 | import com.android.tools.lint.client.api.TYPE_LONG_WRAPPER 42 | import com.android.tools.lint.client.api.TYPE_NULL 43 | import com.android.tools.lint.client.api.TYPE_OBJECT 44 | import com.android.tools.lint.client.api.TYPE_SHORT 45 | import com.android.tools.lint.client.api.TYPE_SHORT_WRAPPER 46 | import com.android.tools.lint.client.api.TYPE_STRING 47 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS 48 | import com.android.tools.lint.detector.api.Category.Companion.MESSAGES 49 | import com.android.tools.lint.detector.api.ConstantEvaluator.evaluateString 50 | import com.android.tools.lint.detector.api.Detector.UastScanner 51 | import com.android.tools.lint.detector.api.Implementation 52 | import com.android.tools.lint.detector.api.Issue 53 | import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE 54 | import com.android.tools.lint.detector.api.Severity.ERROR 55 | import com.android.tools.lint.detector.api.Severity.WARNING 56 | import org.jetbrains.uast.ULiteralExpression 57 | import org.jetbrains.uast.USimpleNameReferenceExpression 58 | import com.intellij.psi.PsiField 59 | import com.intellij.psi.PsiParameter 60 | import java.lang.Byte 61 | import java.lang.Double 62 | import java.lang.Float 63 | import java.lang.IllegalStateException 64 | import java.lang.Long 65 | import java.lang.Short 66 | import java.util.Calendar 67 | import java.util.Date 68 | import java.util.regex.Pattern 69 | 70 | class WrongTimberUsageDetector : Detector(), UastScanner { 71 | override fun getApplicableMethodNames() = listOf("tag", "format", "v", "d", "i", "w", "e", "wtf") 72 | 73 | override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { 74 | val methodName = node.methodName 75 | val evaluator = context.evaluator 76 | 77 | if ("format" == methodName && 78 | (evaluator.isMemberInClass(method, "java.lang.String") || 79 | evaluator.isMemberInClass(method, "kotlin.text.StringsKt__StringsJVMKt")) 80 | ) { 81 | checkNestedStringFormat(context, node) 82 | return 83 | } 84 | if ("tag" == methodName && evaluator.isMemberInClass(method, "timber.log.Timber")) { 85 | checkTagLengthIfMinSdkLessThan26(context, node) 86 | } 87 | if (evaluator.isMemberInClass(method, "android.util.Log")) { 88 | context.report( 89 | Incident( 90 | issue = ISSUE_LOG, 91 | scope = node, 92 | location = context.getLocation(node), 93 | message = "Using 'Log' instead of 'Timber'", 94 | fix = quickFixIssueLog(node) 95 | ) 96 | ) 97 | return 98 | } 99 | // Handles Timber.X(..) and Timber.tag(..).X(..) where X in (v|d|i|w|e|wtf). 100 | if (isTimberLogMethod(method, evaluator)) { 101 | checkMethodArguments(context, node) 102 | checkFormatArguments(context, node) 103 | checkExceptionLogging(context, node) 104 | } 105 | } 106 | 107 | private fun isTimberLogMethod(method: PsiMethod, evaluator: JavaEvaluator): Boolean { 108 | return evaluator.isMemberInClass(method, "timber.log.Timber") 109 | || evaluator.isMemberInClass(method, "timber.log.Timber.Companion") 110 | || evaluator.isMemberInClass(method, "timber.log.Timber.Tree") 111 | } 112 | 113 | private fun checkNestedStringFormat(context: JavaContext, call: UCallExpression) { 114 | var current: UElement? = call 115 | while (true) { 116 | current = skipParentheses(current!!.uastParent) 117 | if (current == null || current is UMethod) { 118 | // Reached AST root or code block node; String.format not inside Timber.X(..). 119 | return 120 | } 121 | if (current.isMethodCall()) { 122 | val psiMethod = (current as UCallExpression).resolve() 123 | if (psiMethod != null && 124 | Pattern.matches(TIMBER_TREE_LOG_METHOD_REGEXP, psiMethod.name) 125 | && isTimberLogMethod(psiMethod, context.evaluator) 126 | ) { 127 | context.report( 128 | Incident( 129 | issue = ISSUE_FORMAT, 130 | scope = call, 131 | location = context.getLocation(call), 132 | message = "Using 'String#format' inside of 'Timber'", 133 | fix = quickFixIssueFormat(call) 134 | ) 135 | ) 136 | return 137 | } 138 | } 139 | } 140 | } 141 | 142 | private fun checkTagLengthIfMinSdkLessThan26(context: JavaContext, call: UCallExpression) { 143 | val argument = call.valueArguments[0] 144 | val tag = evaluateString(context, argument, true) 145 | if (tag != null && tag.length > 23) { 146 | context.report( 147 | Incident( 148 | issue = ISSUE_TAG_LENGTH, 149 | scope = argument, 150 | location = context.getLocation(argument), 151 | message = "The logging tag can be at most 23 characters, was ${tag.length} ($tag)", 152 | fix = quickFixIssueTagLength(argument, tag) 153 | ), 154 | // As of API 26, Log tags are no longer limited to 23 chars. 155 | constraint = minSdkLessThan(26) 156 | ) 157 | } 158 | } 159 | 160 | private fun checkFormatArguments(context: JavaContext, call: UCallExpression) { 161 | val arguments = call.valueArguments 162 | val numArguments = arguments.size 163 | if (numArguments == 0) { 164 | return 165 | } 166 | 167 | var startIndexOfArguments = 1 168 | var formatStringArg = arguments[0] 169 | if (isSubclassOf(context, formatStringArg, Throwable::class.java)) { 170 | if (numArguments == 1) { 171 | return 172 | } 173 | formatStringArg = arguments[1] 174 | startIndexOfArguments++ 175 | } 176 | 177 | val formatString = evaluateString(context, formatStringArg, false) 178 | ?: return // We passed for example a method call 179 | 180 | val formatArgumentCount = getFormatArgumentCount(formatString) 181 | val passedArgCount = numArguments - startIndexOfArguments 182 | if (formatArgumentCount < passedArgCount) { 183 | context.report( 184 | Incident( 185 | issue = ISSUE_ARG_COUNT, 186 | scope = call, 187 | location = context.getLocation(call), 188 | message = "Wrong argument count, format string `${formatString}` requires `${formatArgumentCount}` but format call supplies `${passedArgCount}`" 189 | ) 190 | ) 191 | return 192 | } 193 | 194 | if (formatArgumentCount == 0) { 195 | return 196 | } 197 | 198 | val types = getStringArgumentTypes(formatString) 199 | var argument: UExpression? = null 200 | var argumentIndex = startIndexOfArguments 201 | var valid: Boolean 202 | for (i in types.indices) { 203 | val formatType = types[i] 204 | if (argumentIndex != numArguments) { 205 | argument = arguments[argumentIndex++] 206 | } else { 207 | context.report( 208 | Incident( 209 | issue = ISSUE_ARG_COUNT, 210 | scope = call, 211 | location = context.getLocation(call), 212 | message = "Wrong argument count, format string `${formatString}` requires `${formatArgumentCount}` but format call supplies `${passedArgCount}`" 213 | ) 214 | ) 215 | } 216 | 217 | val type = getType(argument) ?: continue 218 | val last = formatType.last() 219 | if (formatType.length >= 2 && formatType[formatType.length - 2].toLowerCase() == 't') { 220 | // Date time conversion. 221 | when (last) { 222 | 'H', 'I', 'k', 'l', 'M', 'S', 'L', 'N', 'p', 'z', 'Z', 's', 'Q', // time 223 | 'B', 'b', 'h', 'A', 'a', 'C', 'Y', 'y', 'j', 'm', 'd', 'e', // date 224 | 'R', 'T', 'r', 'D', 'F', 'c' -> { // date/time 225 | valid = 226 | type == Integer.TYPE || type == Calendar::class.java || type == Date::class.java || type == java.lang.Long.TYPE 227 | if (!valid) { 228 | context.report( 229 | Incident( 230 | issue = ISSUE_ARG_TYPES, 231 | scope = call, 232 | location = context.getLocation(argument), 233 | message = "Wrong argument type for date formatting argument '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)" 234 | ) 235 | ) 236 | } 237 | } 238 | else -> { 239 | context.report( 240 | Incident( 241 | issue = ISSUE_FORMAT, 242 | scope = call, 243 | location = context.getLocation(argument), 244 | message = "Wrong suffix for date format '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)" 245 | ) 246 | ) 247 | } 248 | } 249 | continue 250 | } 251 | 252 | valid = when (last) { 253 | 'b', 'B' -> type == java.lang.Boolean.TYPE 254 | 'x', 'X', 'd', 'o', 'e', 'E', 'f', 'g', 'G', 'a', 'A' -> { 255 | type == Integer.TYPE || type == java.lang.Float.TYPE || type == java.lang.Double.TYPE || type == java.lang.Long.TYPE || type == java.lang.Byte.TYPE || type == java.lang.Short.TYPE 256 | } 257 | 'c', 'C' -> type == Character.TYPE 258 | 'h', 'H' -> type != java.lang.Boolean.TYPE && !Number::class.java.isAssignableFrom(type) 259 | 's', 'S' -> true 260 | else -> true 261 | } 262 | if (!valid) { 263 | context.report( 264 | Incident( 265 | issue = ISSUE_ARG_TYPES, 266 | scope = call, 267 | location = context.getLocation(argument), 268 | message = "Wrong argument type for formatting argument '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)" 269 | ) 270 | ) 271 | } 272 | } 273 | } 274 | 275 | private fun getType(expression: UExpression?): Class<*>? { 276 | if (expression == null) { 277 | return null 278 | } 279 | if (expression is PsiMethodCallExpression) { 280 | val call = expression as PsiMethodCallExpression 281 | val method = call.resolveMethod() ?: return null 282 | val methodName = method.name 283 | if (methodName == GET_STRING_METHOD) { 284 | return String::class.java 285 | } 286 | } else if (expression is PsiLiteralExpression) { 287 | val literalExpression = expression as PsiLiteralExpression 288 | val expressionType = literalExpression.type 289 | when { 290 | isString(expressionType!!) -> return String::class.java 291 | expressionType === PsiType.INT -> return Integer.TYPE 292 | expressionType === PsiType.FLOAT -> return java.lang.Float.TYPE 293 | expressionType === PsiType.CHAR -> return Character.TYPE 294 | expressionType === PsiType.BOOLEAN -> return java.lang.Boolean.TYPE 295 | expressionType === PsiType.NULL -> return Any::class.java 296 | } 297 | } 298 | 299 | val type = expression.getExpressionType() 300 | if (type != null) { 301 | val typeClass = getTypeClass(type) 302 | return typeClass ?: Any::class.java 303 | } 304 | 305 | return null 306 | } 307 | 308 | private fun getTypeClass(type: PsiType?): Class<*>? { 309 | return when (type?.canonicalText) { 310 | null -> null 311 | TYPE_STRING, "String" -> String::class.java 312 | TYPE_INT -> Integer.TYPE 313 | TYPE_BOOLEAN -> java.lang.Boolean.TYPE 314 | TYPE_NULL -> Object::class.java 315 | TYPE_LONG -> Long.TYPE 316 | TYPE_FLOAT -> Float.TYPE 317 | TYPE_DOUBLE -> Double.TYPE 318 | TYPE_CHAR -> Character.TYPE 319 | TYPE_OBJECT -> null 320 | TYPE_INTEGER_WRAPPER, TYPE_SHORT_WRAPPER, TYPE_BYTE_WRAPPER, TYPE_LONG_WRAPPER -> Integer.TYPE 321 | TYPE_FLOAT_WRAPPER, TYPE_DOUBLE_WRAPPER -> Float.TYPE 322 | TYPE_BOOLEAN_WRAPPER -> java.lang.Boolean.TYPE 323 | TYPE_BYTE -> Byte.TYPE 324 | TYPE_SHORT -> Short.TYPE 325 | "Date", "java.util.Date" -> Date::class.java 326 | "Calendar", "java.util.Calendar" -> Calendar::class.java 327 | "BigDecimal", "java.math.BigDecimal" -> Float.TYPE 328 | "BigInteger", "java.math.BigInteger" -> Integer.TYPE 329 | else -> null 330 | } 331 | } 332 | 333 | private fun isSubclassOf( 334 | context: JavaContext, expression: UExpression, cls: Class<*> 335 | ): Boolean { 336 | val expressionType = expression.getExpressionType() 337 | if (expressionType is PsiClassType) { 338 | return context.evaluator.extendsClass(expressionType.resolve(), cls.name, false) 339 | } 340 | return false 341 | } 342 | 343 | private fun getStringArgumentTypes(formatString: String): List { 344 | val types = mutableListOf() 345 | val matcher = StringFormatDetector.FORMAT.matcher(formatString) 346 | var index = 0 347 | var prevIndex = 0 348 | 349 | while (true) { 350 | if (matcher.find(index)) { 351 | val matchStart = matcher.start() 352 | while (prevIndex < matchStart) { 353 | val c = formatString[prevIndex] 354 | if (c == '\\') { 355 | prevIndex++ 356 | } 357 | prevIndex++ 358 | } 359 | if (prevIndex > matchStart) { 360 | index = prevIndex 361 | continue 362 | } 363 | 364 | index = matcher.end() 365 | val str = formatString.substring(matchStart, matcher.end()) 366 | if ("%%" == str || "%n" == str) { 367 | continue 368 | } 369 | val time = matcher.group(5) 370 | types += if ("t".equals(time, ignoreCase = true)) { 371 | time + matcher.group(6) 372 | } else { 373 | matcher.group(6) 374 | } 375 | } else { 376 | break 377 | } 378 | } 379 | return types 380 | } 381 | 382 | private fun getFormatArgumentCount(s: String): Int { 383 | val matcher = StringFormatDetector.FORMAT.matcher(s) 384 | var index = 0 385 | var prevIndex = 0 386 | var nextNumber = 1 387 | var max = 0 388 | while (true) { 389 | if (matcher.find(index)) { 390 | val value = matcher.group(6) 391 | if ("%" == value || "n" == value) { 392 | index = matcher.end() 393 | continue 394 | } 395 | val matchStart = matcher.start() 396 | while (prevIndex < matchStart) { 397 | val c = s[prevIndex] 398 | if (c == '\\') { 399 | prevIndex++ 400 | } 401 | prevIndex++ 402 | } 403 | if (prevIndex > matchStart) { 404 | index = prevIndex 405 | continue 406 | } 407 | 408 | var number: Int 409 | var numberString = matcher.group(1) 410 | if (numberString != null) { 411 | // Strip off trailing $ 412 | numberString = numberString.substring(0, numberString.length - 1) 413 | number = numberString.toInt() 414 | nextNumber = number + 1 415 | } else { 416 | number = nextNumber++ 417 | } 418 | if (number > max) { 419 | max = number 420 | } 421 | index = matcher.end() 422 | } else { 423 | break 424 | } 425 | } 426 | return max 427 | } 428 | 429 | private fun checkMethodArguments(context: JavaContext, call: UCallExpression) { 430 | call.valueArguments.forEachIndexed loop@{ i, argument -> 431 | if (checkElement(context, call, argument)) return@loop 432 | 433 | if (i > 0 && isSubclassOf(context, argument, Throwable::class.java)) { 434 | context.report( 435 | Incident( 436 | issue = ISSUE_THROWABLE, 437 | scope = call, 438 | location = context.getLocation(call), 439 | message = "Throwable should be first argument", 440 | fix = quickFixIssueThrowable(call, call.valueArguments, argument) 441 | ) 442 | ) 443 | } 444 | } 445 | } 446 | 447 | private fun checkExceptionLogging(context: JavaContext, call: UCallExpression) { 448 | val arguments = call.valueArguments 449 | val numArguments = arguments.size 450 | if (numArguments > 1 && isSubclassOf(context, arguments[0], Throwable::class.java)) { 451 | val messageArg = arguments[1] 452 | 453 | if (isLoggingExceptionMessage(context, messageArg)) { 454 | context.report( 455 | Incident( 456 | issue = ISSUE_EXCEPTION_LOGGING, 457 | scope = messageArg, 458 | location = context.getLocation(call), 459 | message = "Explicitly logging exception message is redundant", 460 | fix = quickFixRemoveRedundantArgument(messageArg) 461 | ) 462 | ) 463 | return 464 | } 465 | 466 | val s = evaluateString(context, messageArg, true) 467 | if (s == null && !canEvaluateExpression(messageArg)) { 468 | // Parameters and non-final fields can't be evaluated. 469 | return 470 | } 471 | 472 | if (s == null || s.isEmpty()) { 473 | context.report( 474 | Incident( 475 | issue = ISSUE_EXCEPTION_LOGGING, 476 | scope = messageArg, 477 | location = context.getLocation(call), 478 | message = "Use single-argument log method instead of null/empty message", 479 | fix = quickFixRemoveRedundantArgument(messageArg) 480 | ) 481 | ) 482 | } 483 | } else if (numArguments == 1 && !isSubclassOf(context, arguments[0], Throwable::class.java)) { 484 | val messageArg = arguments[0] 485 | 486 | if (isLoggingExceptionMessage(context, messageArg)) { 487 | context.report( 488 | Incident( 489 | issue = ISSUE_EXCEPTION_LOGGING, 490 | scope = messageArg, 491 | location = context.getLocation(call), 492 | message = "Explicitly logging exception message is redundant", 493 | fix = quickFixReplaceMessageWithThrowable(messageArg) 494 | ) 495 | ) 496 | } 497 | } 498 | } 499 | 500 | private fun isLoggingExceptionMessage(context: JavaContext, arg: UExpression): Boolean { 501 | if (arg !is UQualifiedReferenceExpression) { 502 | return false 503 | } 504 | 505 | val psi = arg.sourcePsi 506 | if (psi != null && isKotlin(psi.language)) { 507 | return isPropertyOnSubclassOf(context, arg, "message", Throwable::class.java) 508 | } 509 | 510 | val selector = arg.selector 511 | 512 | // what other UExpressions could be a selector? 513 | return if (selector !is UCallExpression) { 514 | false 515 | } else isCallFromMethodInSubclassOf( 516 | context = context, 517 | call = selector, 518 | methodName = "getMessage", 519 | classType = Throwable::class.java 520 | ) 521 | } 522 | 523 | private fun canEvaluateExpression(expression: UExpression): Boolean { 524 | // TODO - try using CallGraph? 525 | if (expression is ULiteralExpression) { 526 | return true 527 | } 528 | if (expression !is USimpleNameReferenceExpression) { 529 | return false 530 | } 531 | val resolvedElement = expression.resolve() 532 | return !(resolvedElement is PsiField || resolvedElement is PsiParameter) 533 | } 534 | 535 | private fun isCallFromMethodInSubclassOf( 536 | context: JavaContext, call: UCallExpression, methodName: String, classType: Class<*> 537 | ): Boolean { 538 | val method = call.resolve() 539 | return method != null 540 | && methodName == call.methodName 541 | && context.evaluator.isMemberInSubClassOf(method, classType.canonicalName, false) 542 | } 543 | 544 | private fun isPropertyOnSubclassOf( 545 | context: JavaContext, 546 | expression: UQualifiedReferenceExpression, 547 | propertyName: String, 548 | classType: Class<*> 549 | ): Boolean { 550 | return isSubclassOf(context, expression.receiver, classType) 551 | && expression.selector.asSourceString() == propertyName 552 | } 553 | 554 | private fun checkElement( 555 | context: JavaContext, call: UCallExpression, element: UElement? 556 | ): Boolean { 557 | if (element is UBinaryExpression) { 558 | val operator = element.operator 559 | if (operator === UastBinaryOperator.PLUS || operator === UastBinaryOperator.PLUS_ASSIGN) { 560 | val argumentType = getType(element) 561 | if (argumentType == String::class.java) { 562 | if (element.leftOperand.isInjectionHost() 563 | && element.rightOperand.isInjectionHost() 564 | ) { 565 | return false 566 | } 567 | context.report( 568 | Incident( 569 | issue = ISSUE_BINARY, 570 | scope = call, 571 | location = context.getLocation(element), 572 | message = "Replace String concatenation with Timber's string formatting", 573 | fix = quickFixIssueBinary(element) 574 | ) 575 | ) 576 | return true 577 | } 578 | } 579 | } else if (element is UIfExpression) { 580 | return checkConditionalUsage(context, call, element) 581 | } 582 | return false 583 | } 584 | 585 | private fun checkConditionalUsage( 586 | context: JavaContext, call: UCallExpression, element: UElement 587 | ): Boolean { 588 | return if (element is UIfExpression) { 589 | if (checkElement(context, call, element.thenExpression)) { 590 | false 591 | } else { 592 | checkElement(context, call, element.elseExpression) 593 | } 594 | } else { 595 | false 596 | } 597 | } 598 | 599 | private fun quickFixIssueLog(logCall: UCallExpression): LintFix { 600 | val arguments = logCall.valueArguments 601 | val methodName = logCall.methodName 602 | val tag = arguments[0] 603 | 604 | // 1st suggestion respects author's tag preference. 605 | // 2nd suggestion drops it (Timber defaults to calling class name). 606 | var fixSource1 = "Timber.tag(${tag.asSourceString()})." 607 | var fixSource2 = "Timber." 608 | 609 | when (arguments.size) { 610 | 2 -> { 611 | val msgOrThrowable = arguments[1] 612 | fixSource1 += "$methodName(${msgOrThrowable.asSourceString()})" 613 | fixSource2 += "$methodName(${msgOrThrowable.asSourceString()})" 614 | } 615 | 3 -> { 616 | val msg = arguments[1] 617 | val throwable = arguments[2] 618 | fixSource1 += "$methodName(${throwable.sourcePsi?.text}, ${msg.asSourceString()})" 619 | fixSource2 += "$methodName(${throwable.sourcePsi?.text}, ${msg.asSourceString()})" 620 | } 621 | else -> { 622 | throw IllegalStateException("android.util.Log overloads should have 2 or 3 arguments") 623 | } 624 | } 625 | 626 | val logCallSource = logCall.uastParent!!.sourcePsi?.text 627 | return fix().group() 628 | .add( 629 | fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource1).build() 630 | ) 631 | .add( 632 | fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource2).build() 633 | ) 634 | .build() 635 | } 636 | 637 | private fun quickFixIssueFormat(stringFormatCall: UCallExpression): LintFix { 638 | // Handles: 639 | // 1) String.format(..) 640 | // 2) format(...) [static import] 641 | val callReceiver = stringFormatCall.receiver 642 | var callSourceString = if (callReceiver == null) "" else "${callReceiver.asSourceString()}." 643 | callSourceString += stringFormatCall.methodName 644 | 645 | return fix().name("Remove String.format(...)").composite() // 646 | // Delete closing parenthesis of String.format(...) 647 | .add(fix().replace().pattern("$callSourceString\\(.*(\\))").with("").build()) 648 | // Delete "String.format(" 649 | .add(fix().replace().text("$callSourceString(").with("").build()).build() 650 | } 651 | 652 | private fun quickFixIssueThrowable( 653 | call: UCallExpression, arguments: List, throwable: UExpression 654 | ): LintFix { 655 | val rearrangedArgs = buildString { 656 | append(throwable.asSourceString()) 657 | arguments.forEach { arg -> 658 | if (arg !== throwable) { 659 | append(", ${arg.asSourceString()}") 660 | } 661 | } 662 | } 663 | return fix() 664 | .replace() 665 | .pattern("\\." + call.methodName + "\\((.*)\\)") 666 | .with(rearrangedArgs) 667 | .build() 668 | } 669 | 670 | private fun quickFixIssueBinary(binaryExpression: UBinaryExpression): LintFix { 671 | val leftOperand = binaryExpression.leftOperand 672 | val rightOperand = binaryExpression.rightOperand 673 | val isLeftLiteral = leftOperand.isInjectionHost() 674 | val isRightLiteral = rightOperand.isInjectionHost() 675 | 676 | // "a" + "b" => "ab" 677 | if (isLeftLiteral && isRightLiteral) { 678 | return fix().replace() 679 | .text(binaryExpression.asSourceString()) 680 | .with("\"${binaryExpression.evaluateString()}\"") 681 | .build() 682 | } 683 | 684 | val args: String = when { 685 | isLeftLiteral -> { 686 | "\"${leftOperand.evaluateString()}%s\", ${rightOperand.asSourceString()}" 687 | } 688 | isRightLiteral -> { 689 | "\"%s${rightOperand.evaluateString()}\", ${leftOperand.asSourceString()}" 690 | } 691 | else -> { 692 | "\"%s%s\", ${leftOperand.asSourceString()}, ${rightOperand.asSourceString()}" 693 | } 694 | } 695 | return fix().replace().text(binaryExpression.asSourceString()).with(args).build() 696 | } 697 | 698 | private fun quickFixIssueTagLength(argument: UExpression, tag: String): LintFix { 699 | val numCharsToTrim = tag.length - 23 700 | return fix().replace() 701 | .name("Strip last " + if (numCharsToTrim == 1) "char" else "$numCharsToTrim chars") 702 | .text(argument.asSourceString()) 703 | .with("\"${tag.substring(0, 23)}\"") 704 | .build() 705 | } 706 | 707 | private fun quickFixRemoveRedundantArgument(arg: UExpression): LintFix { 708 | return fix().replace() 709 | .name("Remove redundant argument") 710 | .text(", ${arg.asSourceString()}") 711 | .with("") 712 | .build() 713 | } 714 | 715 | private fun quickFixReplaceMessageWithThrowable(arg: UExpression): LintFix { 716 | // guaranteed based on callers of this method 717 | val receiver = (arg as UQualifiedReferenceExpression).receiver 718 | return fix().replace() 719 | .name("Replace message with throwable") 720 | .text(arg.asSourceString()) 721 | .with(receiver.asSourceString()) 722 | .build() 723 | } 724 | 725 | companion object { 726 | private const val GET_STRING_METHOD = "getString" 727 | private const val TIMBER_TREE_LOG_METHOD_REGEXP = "(v|d|i|w|e|wtf)" 728 | 729 | val ISSUE_LOG = Issue.create( 730 | id = "LogNotTimber", 731 | briefDescription = "Logging call to Log instead of Timber", 732 | explanation = "Since Timber is included in the project, it is likely that calls to Log should instead be going to Timber.", 733 | category = MESSAGES, 734 | priority = 5, 735 | severity = WARNING, 736 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 737 | ) 738 | val ISSUE_FORMAT = Issue.create( 739 | id = "StringFormatInTimber", 740 | briefDescription = "Logging call with Timber contains String#format()", 741 | explanation = "Since Timber handles String.format automatically, you may not use String#format().", 742 | category = MESSAGES, 743 | priority = 5, 744 | severity = WARNING, 745 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 746 | ) 747 | val ISSUE_THROWABLE = Issue.create( 748 | id = "ThrowableNotAtBeginning", 749 | briefDescription = "Exception in Timber not at the beginning", 750 | explanation = "In Timber you have to pass a Throwable at the beginning of the call.", 751 | category = MESSAGES, 752 | priority = 5, 753 | severity = WARNING, 754 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 755 | ) 756 | val ISSUE_BINARY = Issue.create( 757 | id = "BinaryOperationInTimber", 758 | briefDescription = "Use String#format()", 759 | explanation = "Since Timber handles String#format() automatically, use this instead of String concatenation.", 760 | category = MESSAGES, 761 | priority = 5, 762 | severity = WARNING, 763 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 764 | ) 765 | val ISSUE_ARG_COUNT = Issue.create( 766 | id = "TimberArgCount", 767 | briefDescription = "Formatting argument types incomplete or inconsistent", 768 | explanation = "When a formatted string takes arguments, you need to pass at least that amount of arguments to the formatting call.", 769 | category = MESSAGES, 770 | priority = 9, 771 | severity = ERROR, 772 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 773 | ) 774 | val ISSUE_ARG_TYPES = Issue.create( 775 | id = "TimberArgTypes", 776 | briefDescription = "Formatting string doesn't match passed arguments", 777 | explanation = "The argument types that you specified in your formatting string does not match the types of the arguments that you passed to your formatting call.", 778 | category = MESSAGES, 779 | priority = 9, 780 | severity = ERROR, 781 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 782 | ) 783 | val ISSUE_TAG_LENGTH = Issue.create( 784 | id = "TimberTagLength", 785 | briefDescription = "Too Long Log Tags", 786 | explanation = "Log tags are only allowed to be at most" + " 23 tag characters long.", 787 | category = CORRECTNESS, 788 | priority = 5, 789 | severity = ERROR, 790 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 791 | ) 792 | val ISSUE_EXCEPTION_LOGGING = Issue.create( 793 | id = "TimberExceptionLogging", 794 | briefDescription = "Exception Logging", 795 | explanation = "Explicitly including the exception message is redundant when supplying an exception to log.", 796 | category = CORRECTNESS, 797 | priority = 3, 798 | severity = WARNING, 799 | implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE) 800 | ) 801 | 802 | val issues = arrayOf( 803 | ISSUE_LOG, ISSUE_FORMAT, ISSUE_THROWABLE, ISSUE_BINARY, ISSUE_ARG_COUNT, ISSUE_ARG_TYPES, 804 | ISSUE_TAG_LENGTH, ISSUE_EXCEPTION_LOGGING 805 | ) 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /timber-lint/src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt: -------------------------------------------------------------------------------- 1 | package timber.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.TestFiles.java 4 | import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin 5 | import com.android.tools.lint.checks.infrastructure.TestFiles.manifest 6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint 7 | import org.junit.Test 8 | import timber.lint.WrongTimberUsageDetector.Companion.issues 9 | 10 | class WrongTimberUsageDetectorTest { 11 | private val TIMBER_STUB = kotlin(""" 12 | |package timber.log 13 | |class Timber private constructor() { 14 | | private companion object { 15 | | @JvmStatic fun d(message: String?, vararg args: Any?) {} 16 | | @JvmStatic fun d(t: Throwable?, message: String, vararg args: Any?) {} 17 | | @JvmStatic fun tag(tag: String) = Tree() 18 | | } 19 | | open class Tree { 20 | | open fun d(message: String?, vararg args: Any?) {} 21 | | open fun d(t: Throwable?, message: String?, vararg args: Any?) {} 22 | | } 23 | |}""".trimMargin()) 24 | 25 | @Test fun usingAndroidLogWithTwoArguments() { 26 | lint() 27 | .files( 28 | java(""" 29 | |package foo; 30 | |import android.util.Log; 31 | |public class Example { 32 | | public void log() { 33 | | Log.d("TAG", "msg"); 34 | | } 35 | |}""".trimMargin()), 36 | kotlin(""" 37 | |package foo 38 | |import android.util.Log 39 | |class Example { 40 | | fun log() { 41 | | Log.d("TAG", "msg") 42 | | } 43 | |}""".trimMargin()) 44 | ) 45 | .issues(*issues) 46 | .run() 47 | .expect(""" 48 | |src/foo/Example.java:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 49 | | Log.d("TAG", "msg"); 50 | | ~~~~~~~~~~~~~~~~~~~ 51 | |src/foo/Example.kt:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 52 | | Log.d("TAG", "msg") 53 | | ~~~~~~~~~~~~~~~~~~~ 54 | |0 errors, 2 warnings""".trimMargin()) 55 | .expectFixDiffs(""" 56 | |Fix for src/foo/Example.java line 5: Replace with Timber.tag("TAG").d("msg"): 57 | |@@ -5 +5 58 | |- Log.d("TAG", "msg"); 59 | |+ Timber.tag("TAG").d("msg"); 60 | |Fix for src/foo/Example.java line 5: Replace with Timber.d("msg"): 61 | |@@ -5 +5 62 | |- Log.d("TAG", "msg"); 63 | |+ Timber.d("msg"); 64 | |Fix for src/foo/Example.kt line 5: Replace with Timber.tag("TAG").d("msg"): 65 | |@@ -5 +5 66 | |- Log.d("TAG", "msg") 67 | |+ Timber.tag("TAG").d("msg") 68 | |Fix for src/foo/Example.kt line 5: Replace with Timber.d("msg"): 69 | |@@ -5 +5 70 | |- Log.d("TAG", "msg") 71 | |+ Timber.d("msg") 72 | |""".trimMargin()) 73 | } 74 | 75 | @Test fun usingAndroidLogWithThreeArguments() { 76 | lint() 77 | .files( 78 | java(""" 79 | |package foo; 80 | |import android.util.Log; 81 | |public class Example { 82 | | public void log() { 83 | | Log.d("TAG", "msg", new Exception()); 84 | | } 85 | |}""".trimMargin()), 86 | kotlin(""" 87 | |package foo 88 | |import android.util.Log 89 | |class Example { 90 | | fun log() { 91 | | Log.d("TAG", "msg", Exception()) 92 | | } 93 | |}""".trimMargin()) 94 | ) 95 | .issues(*issues) 96 | .run() 97 | .expect(""" 98 | |src/foo/Example.java:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 99 | | Log.d("TAG", "msg", new Exception()); 100 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | |src/foo/Example.kt:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 102 | | Log.d("TAG", "msg", Exception()) 103 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 104 | |0 errors, 2 warnings""".trimMargin()) 105 | .expectFixDiffs(""" 106 | |Fix for src/foo/Example.java line 5: Replace with Timber.tag("TAG").d(new Exception(), "msg"): 107 | |@@ -5 +5 108 | |- Log.d("TAG", "msg", new Exception()); 109 | |+ Timber.tag("TAG").d(new Exception(), "msg"); 110 | |Fix for src/foo/Example.java line 5: Replace with Timber.d(new Exception(), "msg"): 111 | |@@ -5 +5 112 | |- Log.d("TAG", "msg", new Exception()); 113 | |+ Timber.d(new Exception(), "msg"); 114 | |Fix for src/foo/Example.kt line 5: Replace with Timber.tag("TAG").d(Exception(), "msg"): 115 | |@@ -5 +5 116 | |- Log.d("TAG", "msg", Exception()) 117 | |+ Timber.tag("TAG").d(Exception(), "msg") 118 | |Fix for src/foo/Example.kt line 5: Replace with Timber.d(Exception(), "msg"): 119 | |@@ -5 +5 120 | |- Log.d("TAG", "msg", Exception()) 121 | |+ Timber.d(Exception(), "msg") 122 | |""".trimMargin()) 123 | } 124 | 125 | @Test fun usingFullyQualifiedAndroidLogWithTwoArguments() { 126 | lint() 127 | .files( 128 | java(""" 129 | |package foo; 130 | |public class Example { 131 | | public void log() { 132 | | android.util.Log.d("TAG", "msg"); 133 | | } 134 | |}""".trimMargin()), 135 | kotlin(""" 136 | |package foo 137 | |class Example { 138 | | fun log() { 139 | | android.util.Log.d("TAG", "msg") 140 | | } 141 | |}""".trimMargin()) 142 | ) 143 | .issues(*issues) 144 | .run() 145 | .expect(""" 146 | |src/foo/Example.java:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 147 | | android.util.Log.d("TAG", "msg"); 148 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 149 | |src/foo/Example.kt:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 150 | | android.util.Log.d("TAG", "msg") 151 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 152 | |0 errors, 2 warnings""".trimMargin()) 153 | .expectFixDiffs(""" 154 | |Fix for src/foo/Example.java line 4: Replace with Timber.tag("TAG").d("msg"): 155 | |@@ -4 +4 156 | |- android.util.Log.d("TAG", "msg"); 157 | |+ Timber.tag("TAG").d("msg"); 158 | |Fix for src/foo/Example.java line 4: Replace with Timber.d("msg"): 159 | |@@ -4 +4 160 | |- android.util.Log.d("TAG", "msg"); 161 | |+ Timber.d("msg"); 162 | |Fix for src/foo/Example.kt line 4: Replace with Timber.tag("TAG").d("msg"): 163 | |@@ -4 +4 164 | |- android.util.Log.d("TAG", "msg") 165 | |+ Timber.tag("TAG").d("msg") 166 | |Fix for src/foo/Example.kt line 4: Replace with Timber.d("msg"): 167 | |@@ -4 +4 168 | |- android.util.Log.d("TAG", "msg") 169 | |+ Timber.d("msg") 170 | |""".trimMargin()) 171 | } 172 | 173 | @Test fun usingFullyQualifiedAndroidLogWithThreeArguments() { 174 | lint() 175 | .files( 176 | java(""" 177 | |package foo; 178 | |public class Example { 179 | | public void log() { 180 | | android.util.Log.d("TAG", "msg", new Exception()); 181 | | } 182 | |}""".trimMargin()), 183 | kotlin(""" 184 | |package foo 185 | |class Example { 186 | | fun log() { 187 | | android.util.Log.d("TAG", "msg", Exception()) 188 | | } 189 | |}""".trimMargin()) 190 | ) 191 | .issues(*issues) 192 | .run() 193 | .expect(""" 194 | |src/foo/Example.java:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 195 | | android.util.Log.d("TAG", "msg", new Exception()); 196 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 197 | |src/foo/Example.kt:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber] 198 | | android.util.Log.d("TAG", "msg", Exception()) 199 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 200 | |0 errors, 2 warnings""".trimMargin()) 201 | .expectFixDiffs(""" 202 | |Fix for src/foo/Example.java line 4: Replace with Timber.tag("TAG").d(new Exception(), "msg"): 203 | |@@ -4 +4 204 | |- android.util.Log.d("TAG", "msg", new Exception()); 205 | |+ Timber.tag("TAG").d(new Exception(), "msg"); 206 | |Fix for src/foo/Example.java line 4: Replace with Timber.d(new Exception(), "msg"): 207 | |@@ -4 +4 208 | |- android.util.Log.d("TAG", "msg", new Exception()); 209 | |+ Timber.d(new Exception(), "msg"); 210 | |Fix for src/foo/Example.kt line 4: Replace with Timber.tag("TAG").d(Exception(), "msg"): 211 | |@@ -4 +4 212 | |- android.util.Log.d("TAG", "msg", Exception()) 213 | |+ Timber.tag("TAG").d(Exception(), "msg") 214 | |Fix for src/foo/Example.kt line 4: Replace with Timber.d(Exception(), "msg"): 215 | |@@ -4 +4 216 | |- android.util.Log.d("TAG", "msg", Exception()) 217 | |+ Timber.d(Exception(), "msg") 218 | |""".trimMargin()) 219 | } 220 | 221 | @Test fun innerStringFormat() { 222 | lint() 223 | .files(TIMBER_STUB, 224 | java(""" 225 | |package foo; 226 | |import timber.log.Timber; 227 | |public class Example { 228 | | public void log() { 229 | | Timber.d(String.format("%s", "arg1")); 230 | | } 231 | |}""".trimMargin()), 232 | kotlin(""" 233 | |package foo 234 | |import timber.log.Timber 235 | |class Example { 236 | | fun log() { 237 | | Timber.d(String.format("%s", "arg1")) 238 | | } 239 | |}""".trimMargin()) 240 | ) 241 | .issues(*issues) 242 | .run() 243 | .expect(""" 244 | |src/foo/Example.java:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 245 | | Timber.d(String.format("%s", "arg1")); 246 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 247 | |src/foo/Example.kt:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 248 | | Timber.d(String.format("%s", "arg1")) 249 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 250 | |0 errors, 2 warnings""".trimMargin()) 251 | .expectFixDiffs(""" 252 | |Fix for src/foo/Example.java line 5: Remove String.format(...): 253 | |@@ -5 +5 254 | |- Timber.d(String.format("%s", "arg1")); 255 | |+ Timber.d("%s", "arg1"); 256 | |Fix for src/foo/Example.kt line 5: Remove String.format(...): 257 | |@@ -5 +5 258 | |- Timber.d(String.format("%s", "arg1")) 259 | |+ Timber.d("%s", "arg1") 260 | |""".trimMargin()) 261 | } 262 | 263 | @Test fun innerStringFormatWithStaticImport() { 264 | lint() 265 | .files(TIMBER_STUB, 266 | java(""" 267 | |package foo; 268 | |import timber.log.Timber; 269 | |import static java.lang.String.format; 270 | |public class Example { 271 | | public void log() { 272 | | Timber.d(format("%s", "arg1")); 273 | | } 274 | |}""".trimMargin()), 275 | kotlin(""" 276 | |package foo 277 | |import timber.log.Timber 278 | |import java.lang.String.format 279 | |class Example { 280 | | fun log() { 281 | | Timber.d(format("%s", "arg1")) 282 | | } 283 | |}""".trimMargin()) 284 | ) 285 | // Remove when AGP 7.1.0-alpha07 is out 286 | // https://groups.google.com/g/lint-dev/c/BigCO8sMhKU 287 | .allowCompilationErrors() 288 | .issues(*issues) 289 | .run() 290 | .expect(""" 291 | |src/foo/Example.java:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 292 | | Timber.d(format("%s", "arg1")); 293 | | ~~~~~~~~~~~~~~~~~~~~ 294 | |src/foo/Example.kt:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 295 | | Timber.d(format("%s", "arg1")) 296 | | ~~~~~~~~~~~~~~~~~~~~ 297 | |0 errors, 2 warnings""".trimMargin()) 298 | .expectFixDiffs(""" 299 | |Fix for src/foo/Example.java line 6: Remove String.format(...): 300 | |@@ -6 +6 301 | |- Timber.d(format("%s", "arg1")); 302 | |+ Timber.d("%s", "arg1"); 303 | |Fix for src/foo/Example.kt line 6: Remove String.format(...): 304 | |@@ -6 +6 305 | |- Timber.d(format("%s", "arg1")) 306 | |+ Timber.d("%s", "arg1") 307 | |""".trimMargin()) 308 | } 309 | 310 | @Test fun innerStringFormatInNestedMethods() { 311 | lint() 312 | .files(TIMBER_STUB, 313 | java(""" 314 | |package foo; 315 | |import timber.log.Timber; 316 | |public class Example { 317 | | public void log() { 318 | | Timber.d(id(String.format("%s", "arg1"))); 319 | | } 320 | | private String id(String s) { return s; } 321 | |}""".trimMargin()), 322 | kotlin(""" 323 | |package foo 324 | |import timber.log.Timber 325 | |class Example { 326 | | fun log() { 327 | | Timber.d(id(String.format("%s", "arg1"))) 328 | | } 329 | | private fun id(s: String): String { return s } 330 | |}""".trimMargin()) 331 | ) 332 | .issues(*issues) 333 | .run() 334 | .expect(""" 335 | |src/foo/Example.java:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 336 | | Timber.d(id(String.format("%s", "arg1"))); 337 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 338 | |src/foo/Example.kt:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 339 | | Timber.d(id(String.format("%s", "arg1"))) 340 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 341 | |0 errors, 2 warnings""".trimMargin()) 342 | } 343 | 344 | @Test fun innerStringFormatInNestedAssignment() { 345 | lint() 346 | .files(TIMBER_STUB, 347 | java(""" 348 | |package foo; 349 | |import timber.log.Timber; 350 | |public class Example { 351 | | public void log() { 352 | | String msg = null; 353 | | Timber.d(msg = String.format("msg")); 354 | | } 355 | |}""".trimMargin()) 356 | // no kotlin equivalent, since nested assignments do not exist 357 | ) 358 | .issues(*issues) 359 | .run() 360 | .expect(""" 361 | |src/foo/Example.java:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber] 362 | | Timber.d(msg = String.format("msg")); 363 | | ~~~~~~~~~~~~~~~~~~~~ 364 | |0 errors, 1 warnings""".trimMargin()) 365 | } 366 | 367 | @Test fun validStringFormatInCodeBlock() { 368 | lint() 369 | .files(TIMBER_STUB, 370 | java(""" 371 | |package foo; 372 | |public class Example { 373 | | public void log() { 374 | | for(;;) { 375 | | String name = String.format("msg"); 376 | | } 377 | | } 378 | |}""".trimMargin()), 379 | kotlin(""" 380 | |package foo 381 | |class Example { 382 | | fun log() { 383 | | while(true) { 384 | | val name = String.format("msg") 385 | | } 386 | | } 387 | |}""".trimMargin()) 388 | ) 389 | .issues(*issues) 390 | .run() 391 | .expectClean() 392 | } 393 | 394 | @Test fun validStringFormatInConstructorCall() { 395 | lint() 396 | .files(TIMBER_STUB, 397 | java(""" 398 | |package foo; 399 | |public class Example { 400 | | public void log() { 401 | | new Exception(String.format("msg")); 402 | | } 403 | |}""".trimMargin()), 404 | kotlin(""" 405 | |package foo 406 | |class Example { 407 | | fun log() { 408 | | Exception(String.format("msg")) 409 | | } 410 | |}""".trimMargin()) 411 | ) 412 | .issues(*issues) 413 | .run() 414 | .expectClean() 415 | } 416 | 417 | @Test fun validStringFormatInStaticArray() { 418 | lint() 419 | .files(TIMBER_STUB, 420 | java(""" 421 | |package foo; 422 | |public class Example { 423 | | static String[] X = { String.format("%s", 100) }; 424 | |}""".trimMargin()), 425 | kotlin(""" 426 | |package foo 427 | |class Example { 428 | | companion object { 429 | | val X = arrayOf(String.format("%s", 100)) 430 | | } 431 | |}""".trimMargin()) 432 | ) 433 | .issues(*issues) 434 | .run() 435 | .expectClean() 436 | } 437 | 438 | @Test fun validStringFormatExtracted() { 439 | lint() 440 | .files(TIMBER_STUB, 441 | java(""" 442 | |package foo; 443 | |import timber.log.Timber; 444 | |public class Example { 445 | | public void log() { 446 | | String message = String.format("%s", "foo"); 447 | | Timber.d(message); 448 | | } 449 | |}""".trimMargin()), 450 | kotlin(""" 451 | |package foo 452 | |import timber.log.Timber 453 | |class Example { 454 | | fun log() { 455 | | val message = String.format("%s", "foo") 456 | | Timber.d(message) 457 | | } 458 | |}""".trimMargin()), 459 | ) 460 | .issues(*issues) 461 | .run() 462 | .expectClean() 463 | } 464 | 465 | @Test fun throwableNotAtBeginning() { 466 | lint() 467 | .files(TIMBER_STUB, 468 | java(""" 469 | |package foo; 470 | |import timber.log.Timber; 471 | |public class Example { 472 | | public void log() { 473 | | Exception e = new Exception(); 474 | | Timber.d("%s", e); 475 | | } 476 | |}""".trimMargin()), 477 | kotlin(""" 478 | |package foo 479 | |import timber.log.Timber 480 | |class Example { 481 | | fun log() { 482 | | val e = Exception() 483 | | Timber.d("%s", e) 484 | | } 485 | |}""".trimMargin()) 486 | ) 487 | .issues(*issues) 488 | .run() 489 | .expect(""" 490 | |src/foo/Example.java:6: Warning: Throwable should be first argument [ThrowableNotAtBeginning] 491 | | Timber.d("%s", e); 492 | | ~~~~~~~~~~~~~~~~~ 493 | |src/foo/Example.kt:6: Warning: Throwable should be first argument [ThrowableNotAtBeginning] 494 | | Timber.d("%s", e) 495 | | ~~~~~~~~~~~~~~~~~ 496 | |0 errors, 2 warnings""".trimMargin()) 497 | .expectFixDiffs(""" 498 | |Fix for src/foo/Example.java line 6: Replace with e, "%s": 499 | |@@ -6 +6 500 | |- Timber.d("%s", e); 501 | |+ Timber.d(e, "%s"); 502 | |Fix for src/foo/Example.kt line 6: Replace with e, "%s": 503 | |@@ -6 +6 504 | |- Timber.d("%s", e) 505 | |+ Timber.d(e, "%s") 506 | |""".trimMargin()) 507 | } 508 | 509 | @Test fun stringConcatenationBothLiterals() { 510 | lint() 511 | .files(TIMBER_STUB, 512 | java(""" 513 | |package foo; 514 | |import timber.log.Timber; 515 | |public class Example { 516 | | public void log() { 517 | | Timber.d("foo" + "bar"); 518 | | } 519 | |}""".trimMargin()), 520 | kotlin(""" 521 | |package foo 522 | |import timber.log.Timber 523 | |class Example { 524 | | fun log() { 525 | | Timber.d("foo" + "bar") 526 | | } 527 | |}""".trimMargin()) 528 | ) 529 | .issues(*issues) 530 | .run() 531 | .expectClean() 532 | } 533 | 534 | @Test fun stringConcatenationLeftLiteral() { 535 | lint() 536 | .files(TIMBER_STUB, 537 | java(""" 538 | |package foo; 539 | |import timber.log.Timber; 540 | |public class Example { 541 | | public void log() { 542 | | String foo = "foo"; 543 | | Timber.d(foo + "bar"); 544 | | } 545 | |}""".trimMargin()), 546 | kotlin(""" 547 | |package foo 548 | |import timber.log.Timber 549 | |class Example { 550 | | fun log() { 551 | | val foo = "foo" 552 | | Timber.d("${"$"}{foo}bar") 553 | | } 554 | |}""".trimMargin()) 555 | ) 556 | .issues(*issues) 557 | .run() 558 | .expect(""" 559 | |src/foo/Example.java:6: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber] 560 | | Timber.d(foo + "bar"); 561 | | ~~~~~~~~~~~ 562 | |0 errors, 1 warnings""".trimMargin()) 563 | .expectFixDiffs(""" 564 | |Fix for src/foo/Example.java line 5: Replace with "%sbar", foo: 565 | |@@ -6 +6 566 | |- Timber.d(foo + "bar"); 567 | |+ Timber.d("%sbar", foo); 568 | |""".trimMargin()) 569 | } 570 | 571 | @Test fun stringConcatenationRightLiteral() { 572 | lint() 573 | .files(TIMBER_STUB, 574 | java(""" 575 | |package foo; 576 | |import timber.log.Timber; 577 | |public class Example { 578 | | public void log() { 579 | | String bar = "bar"; 580 | | Timber.d("foo" + bar); 581 | | } 582 | |}""".trimMargin()), 583 | kotlin(""" 584 | |package foo 585 | |import timber.log.Timber 586 | |class Example { 587 | | fun log() { 588 | | val bar = "bar" 589 | | Timber.d("foo${"$"}bar") 590 | | } 591 | |}""".trimMargin()) 592 | ) 593 | .issues(*issues) 594 | .run() 595 | .expect(""" 596 | |src/foo/Example.java:6: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber] 597 | | Timber.d("foo" + bar); 598 | | ~~~~~~~~~~~ 599 | |0 errors, 1 warnings""".trimMargin()) 600 | .expectFixDiffs(""" 601 | |Fix for src/foo/Example.java line 5: Replace with "foo%s", bar: 602 | |@@ -6 +6 603 | |- Timber.d("foo" + bar); 604 | |+ Timber.d("foo%s", bar); 605 | |""".trimMargin()) 606 | } 607 | 608 | @Test fun stringConcatenationBothVariables() { 609 | lint() 610 | .files(TIMBER_STUB, 611 | java(""" 612 | |package foo; 613 | |import timber.log.Timber; 614 | |public class Example { 615 | | public void log() { 616 | | String foo = "foo"; 617 | | String bar = "bar"; 618 | | Timber.d(foo + bar); 619 | | } 620 | |}""".trimMargin()), 621 | kotlin(""" 622 | |package foo 623 | |import timber.log.Timber 624 | |class Example { 625 | | fun log() { 626 | | val foo = "foo" 627 | | val bar = "bar" 628 | | Timber.d("${"$"}foo${"$"}bar") 629 | | } 630 | |}""".trimMargin()) 631 | ) 632 | .issues(*issues) 633 | .run() 634 | .expect(""" 635 | |src/foo/Example.java:7: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber] 636 | | Timber.d(foo + bar); 637 | | ~~~~~~~~~ 638 | |0 errors, 1 warnings""".trimMargin()) 639 | .expectFixDiffs(""" 640 | |Fix for src/foo/Example.java line 6: Replace with "%s%s", foo, bar: 641 | |@@ -7 +7 642 | |- Timber.d(foo + bar); 643 | |+ Timber.d("%s%s", foo, bar); 644 | |""".trimMargin()) 645 | } 646 | 647 | @Test fun stringConcatenationInsideTernary() { 648 | lint() 649 | .files(TIMBER_STUB, 650 | java(""" 651 | |package foo; 652 | |import timber.log.Timber; 653 | |public class Example { 654 | | public void log() { 655 | | String s = "world!"; 656 | | Timber.d(true ? "Hello, " + s : "Bye"); 657 | | } 658 | |}""".trimMargin()), 659 | kotlin(""" 660 | |package foo 661 | |import timber.log.Timber 662 | |class Example { 663 | | fun log() { 664 | | val s = "world!" 665 | | Timber.d(if(true) "Hello, ${"$"}s" else "Bye") 666 | | } 667 | |}""".trimMargin()) 668 | ) 669 | .issues(*issues) 670 | .run() 671 | .expect(""" 672 | |src/foo/Example.java:6: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber] 673 | | Timber.d(true ? "Hello, " + s : "Bye"); 674 | | ~~~~~~~~~~~~~ 675 | |0 errors, 1 warnings""".trimMargin()) 676 | } 677 | 678 | @Test fun tooManyFormatArgs() { 679 | lint() 680 | .files(TIMBER_STUB, 681 | java(""" 682 | |package foo; 683 | |import timber.log.Timber; 684 | |public class Example { 685 | | public void log() { 686 | | Timber.d("%s %s", "arg1"); 687 | | } 688 | |}""".trimMargin()), 689 | kotlin(""" 690 | |package foo 691 | |import timber.log.Timber 692 | |class Example { 693 | | fun log() { 694 | | Timber.d("%s %s", "arg1") 695 | | } 696 | |}""".trimMargin()) 697 | ) 698 | .issues(*issues) 699 | .run() 700 | .expect(""" 701 | |src/foo/Example.java:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount] 702 | | Timber.d("%s %s", "arg1"); 703 | | ~~~~~~~~~~~~~~~~~~~~~~~~~ 704 | |src/foo/Example.kt:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount] 705 | | Timber.d("%s %s", "arg1") 706 | | ~~~~~~~~~~~~~~~~~~~~~~~~~ 707 | |2 errors, 0 warnings""".trimMargin()) 708 | } 709 | 710 | @Test fun tooManyArgs() { 711 | lint() 712 | .files(TIMBER_STUB, 713 | java(""" 714 | |package foo; 715 | |import timber.log.Timber; 716 | |public class Example { 717 | | public void log() { 718 | | Timber.d("%s", "arg1", "arg2"); 719 | | } 720 | |}""".trimMargin()), 721 | kotlin(""" 722 | |package foo 723 | |import timber.log.Timber 724 | |class Example { 725 | | fun log() { 726 | | Timber.d("%s", "arg1", "arg2") 727 | | } 728 | |}""".trimMargin()) 729 | ) 730 | .issues(*issues) 731 | .run() 732 | .expect(""" 733 | |src/foo/Example.java:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount] 734 | | Timber.d("%s", "arg1", "arg2"); 735 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 736 | |src/foo/Example.kt:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount] 737 | | Timber.d("%s", "arg1", "arg2") 738 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 739 | |2 errors, 0 warnings""".trimMargin()) 740 | } 741 | 742 | @Test fun wrongArgTypes() { 743 | lint() 744 | .files(TIMBER_STUB, 745 | java(""" 746 | |package foo; 747 | |import timber.log.Timber; 748 | |public class Example { 749 | | public void log() { 750 | | Timber.d("%d", "arg1"); 751 | | } 752 | |}""".trimMargin()), 753 | kotlin(""" 754 | |package foo 755 | |import timber.log.Timber 756 | |class Example { 757 | | fun log() { 758 | | Timber.d("%d", "arg1") 759 | | } 760 | |}""".trimMargin()) 761 | ) 762 | .issues(*issues) 763 | .run() 764 | .expect(""" 765 | |src/foo/Example.java:5: Error: Wrong argument type for formatting argument '#1' in %d: conversion is 'd', received String (argument #2 in method call) [TimberArgTypes] 766 | | Timber.d("%d", "arg1"); 767 | | ~~~~~~ 768 | |src/foo/Example.kt:5: Error: Wrong argument type for formatting argument '#1' in %d: conversion is 'd', received String (argument #2 in method call) [TimberArgTypes] 769 | | Timber.d("%d", "arg1") 770 | | ~~~~ 771 | |2 errors, 0 warnings""".trimMargin()) 772 | } 773 | 774 | @Test fun tagTooLongLiteralOnly() { 775 | lint() 776 | .files(TIMBER_STUB, 777 | java(""" 778 | |package foo; 779 | |import timber.log.Timber; 780 | |public class Example { 781 | | public void log() { 782 | | Timber.tag("abcdefghijklmnopqrstuvwx"); 783 | | } 784 | |}""".trimMargin()), 785 | kotlin(""" 786 | |package foo 787 | |import timber.log.Timber 788 | |class Example { 789 | | fun log() { 790 | | Timber.tag("abcdefghijklmnopqrstuvwx") 791 | | } 792 | |}""".trimMargin()), 793 | manifest().minSdk(25) 794 | ) 795 | .issues(*issues) 796 | .run() 797 | .expect(""" 798 | |src/foo/Example.java:5: Error: The logging tag can be at most 23 characters, was 24 (abcdefghijklmnopqrstuvwx) [TimberTagLength] 799 | | Timber.tag("abcdefghijklmnopqrstuvwx"); 800 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 801 | |1 errors, 0 warnings""".trimMargin()) 802 | } 803 | 804 | @Test fun tagTooLongLiteralOnlyBeforeApi26() { 805 | lint() 806 | .files(TIMBER_STUB, 807 | java(""" 808 | |package foo; 809 | |import timber.log.Timber; 810 | |public class Example { 811 | | public void log() { 812 | | Timber.tag("abcdefghijklmnopqrstuvwx"); 813 | | } 814 | |}""".trimMargin()), 815 | kotlin(""" 816 | |package foo 817 | |import timber.log.Timber 818 | |class Example { 819 | | fun log() { 820 | | Timber.tag("abcdefghijklmnopqrstuvwx") 821 | | } 822 | |}""".trimMargin()), 823 | manifest().minSdk(26) 824 | ) 825 | .issues(*issues) 826 | .run() 827 | .expectClean() 828 | } 829 | 830 | @Test fun tooManyFormatArgsInTag() { 831 | lint() 832 | .files(TIMBER_STUB, 833 | java(""" 834 | |package foo; 835 | |import timber.log.Timber; 836 | |public class Example { 837 | | public void log() { 838 | | Timber.tag("tag").d("%s %s", "arg1"); 839 | | } 840 | |}""".trimMargin()), 841 | kotlin(""" 842 | |package foo 843 | |import timber.log.Timber 844 | |class Example { 845 | | fun log() { 846 | | Timber.tag("tag").d("%s %s", "arg1") 847 | | } 848 | |}""".trimMargin()) 849 | ) 850 | .issues(*issues) 851 | .run() 852 | .expect(""" 853 | |src/foo/Example.java:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount] 854 | | Timber.tag("tag").d("%s %s", "arg1"); 855 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 856 | |src/foo/Example.kt:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount] 857 | | Timber.tag("tag").d("%s %s", "arg1") 858 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 859 | |2 errors, 0 warnings""".trimMargin()) 860 | } 861 | 862 | @Test fun tooManyArgsInTag() { 863 | lint() 864 | .files(TIMBER_STUB, 865 | java(""" 866 | |package foo; 867 | |import timber.log.Timber; 868 | |public class Example { 869 | | public void log() { 870 | | Timber.tag("tag").d("%s", "arg1", "arg2"); 871 | | } 872 | |}""".trimMargin()), 873 | kotlin(""" 874 | |package foo 875 | |import timber.log.Timber 876 | |class Example { 877 | | fun log() { 878 | | Timber.tag("tag").d("%s", "arg1", "arg2") 879 | | } 880 | |}""".trimMargin()) 881 | ) 882 | .issues(*issues) 883 | .run() 884 | .expect(""" 885 | |src/foo/Example.java:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount] 886 | | Timber.tag("tag").d("%s", "arg1", "arg2"); 887 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 888 | |src/foo/Example.kt:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount] 889 | | Timber.tag("tag").d("%s", "arg1", "arg2") 890 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 891 | |2 errors, 0 warnings""".trimMargin()) 892 | } 893 | 894 | @Test fun wrongArgTypesInTag() { 895 | lint() 896 | .files(TIMBER_STUB, 897 | java(""" 898 | |package foo; 899 | |import timber.log.Timber; 900 | |public class Example { 901 | | public void log() { 902 | | Timber.tag("tag").d("%d", "arg1"); 903 | | } 904 | |}""".trimMargin()), 905 | kotlin(""" 906 | |package foo 907 | |import timber.log.Timber 908 | |class Example { 909 | | fun log() { 910 | | Timber.tag("tag").d("%d", "arg1") 911 | | } 912 | |}""".trimMargin()) 913 | ) 914 | .issues(*issues) 915 | .run() 916 | .expect(""" 917 | |src/foo/Example.java:5: Error: Wrong argument type for formatting argument '#1' in %d: conversion is 'd', received String (argument #2 in method call) [TimberArgTypes] 918 | | Timber.tag("tag").d("%d", "arg1"); 919 | | ~~~~~~ 920 | |src/foo/Example.kt:5: Error: Wrong argument type for formatting argument '#1' in %d: conversion is 'd', received String (argument #2 in method call) [TimberArgTypes] 921 | | Timber.tag("tag").d("%d", "arg1") 922 | | ~~~~ 923 | |2 errors, 0 warnings""".trimMargin()) 924 | } 925 | 926 | @Test fun exceptionLoggingUsingExceptionMessage() { 927 | lint() 928 | .files(TIMBER_STUB, 929 | java(""" 930 | |package foo; 931 | |import timber.log.Timber; 932 | |public class Example { 933 | | public void log() { 934 | | Exception e = new Exception(); 935 | | Timber.d(e.getMessage()); 936 | | } 937 | |}""".trimMargin()), 938 | kotlin(""" 939 | |package foo 940 | |import timber.log.Timber 941 | |class Example { 942 | | fun log() { 943 | | val e = Exception() 944 | | Timber.d(e.message) 945 | | } 946 | |}""".trimMargin()) 947 | ) 948 | .issues(*issues) 949 | .run() 950 | .expect(""" 951 | |src/foo/Example.java:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging] 952 | | Timber.d(e.getMessage()); 953 | | ~~~~~~~~~~~~~~~~~~~~~~~~ 954 | |src/foo/Example.kt:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging] 955 | | Timber.d(e.message) 956 | | ~~~~~~~~~~~~~~~~~~~ 957 | |0 errors, 2 warnings""".trimMargin()) 958 | .expectFixDiffs(""" 959 | |Fix for src/foo/Example.java line 6: Replace message with throwable: 960 | |@@ -6 +6 961 | |- Timber.d(e.getMessage()); 962 | |+ Timber.d(e); 963 | |Fix for src/foo/Example.kt line 6: Replace message with throwable: 964 | |@@ -6 +6 965 | |- Timber.d(e.message) 966 | |+ Timber.d(e) 967 | |""".trimMargin()) 968 | } 969 | 970 | @Test fun exceptionLoggingUsingExceptionMessageArgument() { 971 | lint() 972 | .files(TIMBER_STUB, 973 | java(""" 974 | |package foo; 975 | |import timber.log.Timber; 976 | |public class Example { 977 | | public void log() { 978 | | Exception e = new Exception(); 979 | | Timber.d(e, e.getMessage()); 980 | | } 981 | |}""".trimMargin()), 982 | kotlin(""" 983 | |package foo 984 | |import timber.log.Timber 985 | |class Example { 986 | | fun log() { 987 | | val e = Exception() 988 | | Timber.d(e, e.message) 989 | | } 990 | |}""".trimMargin()) 991 | ) 992 | .issues(*issues) 993 | .run() 994 | .expect(""" 995 | |src/foo/Example.java:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging] 996 | | Timber.d(e, e.getMessage()); 997 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 998 | |src/foo/Example.kt:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging] 999 | | Timber.d(e, e.message) 1000 | | ~~~~~~~~~~~~~~~~~~~~~~ 1001 | |0 errors, 2 warnings""".trimMargin()) 1002 | .expectFixDiffs(""" 1003 | |Fix for src/foo/Example.java line 5: Remove redundant argument: 1004 | |@@ -6 +6 1005 | |- Timber.d(e, e.getMessage()); 1006 | |+ Timber.d(e); 1007 | |Fix for src/foo/Example.kt line 5: Remove redundant argument: 1008 | |@@ -6 +6 1009 | |- Timber.d(e, e.message) 1010 | |+ Timber.d(e) 1011 | |""".trimMargin()) 1012 | } 1013 | 1014 | @Test fun exceptionLoggingUsingVariable() { 1015 | lint() 1016 | .files(TIMBER_STUB, 1017 | java(""" 1018 | |package foo; 1019 | |import timber.log.Timber; 1020 | |public class Example { 1021 | | public void log() { 1022 | | String msg = "Hello"; 1023 | | Exception e = new Exception(); 1024 | | Timber.d(e, msg); 1025 | | } 1026 | |}""".trimMargin()), 1027 | kotlin(""" 1028 | |package foo 1029 | |import timber.log.Timber 1030 | |class Example { 1031 | | fun log() { 1032 | | val msg = "Hello" 1033 | | val e = Exception() 1034 | | Timber.d(e, msg) 1035 | | } 1036 | |}""".trimMargin()) 1037 | ) 1038 | .issues(*issues) 1039 | .run() 1040 | .expectClean() 1041 | } 1042 | 1043 | @Test fun exceptionLoggingUsingParameter() { 1044 | lint() 1045 | .files(TIMBER_STUB, 1046 | java(""" 1047 | |package foo; 1048 | |import timber.log.Timber; 1049 | |public class Example { 1050 | | public void log(Exception e, String message) { 1051 | | Timber.d(e, message); 1052 | | } 1053 | |}""".trimMargin()), 1054 | kotlin(""" 1055 | |package foo 1056 | |import timber.log.Timber 1057 | |class Example { 1058 | | fun log(e: Exception, message: String) { 1059 | | Timber.d(e, message) 1060 | | } 1061 | |}""".trimMargin()) 1062 | ) 1063 | .issues(*issues) 1064 | .run() 1065 | .expectClean() 1066 | } 1067 | 1068 | @Test fun exceptionLoggingUsingMethod() { 1069 | lint() 1070 | .files(TIMBER_STUB, 1071 | java(""" 1072 | |package foo; 1073 | |import timber.log.Timber; 1074 | |public class Example { 1075 | | public void log(Exception e) { 1076 | | Timber.d(e, method()); 1077 | | } 1078 | | private String method() { 1079 | | return "foo"; 1080 | | } 1081 | |}""".trimMargin()), 1082 | kotlin(""" 1083 | |package foo 1084 | |import timber.log.Timber 1085 | |class Example { 1086 | | fun log(e: Exception) { 1087 | | Timber.d(e, method()) 1088 | | } 1089 | | private fun method(): String { 1090 | | return "foo" 1091 | | } 1092 | |}""".trimMargin()) 1093 | ) 1094 | .issues(*issues) 1095 | .run() 1096 | .expectClean() 1097 | } 1098 | 1099 | @Test fun exceptionLoggingUsingNonFinalField() { 1100 | lint() 1101 | .files(TIMBER_STUB, 1102 | java(""" 1103 | |package foo; 1104 | |import timber.log.Timber; 1105 | |public class Example { 1106 | | private String message; 1107 | | public void log() { 1108 | | Exception e = new Exception(); 1109 | | Timber.d(e, message); 1110 | | } 1111 | |}""".trimMargin()), 1112 | kotlin(""" 1113 | |package foo 1114 | |import timber.log.Timber 1115 | |class Example { 1116 | | private var message = "" 1117 | | fun log() { 1118 | | val e = Exception() 1119 | | Timber.d(e, message) 1120 | | } 1121 | |}""".trimMargin()) 1122 | ) 1123 | .issues(*issues) 1124 | .run() 1125 | .expectClean() 1126 | } 1127 | 1128 | @Test fun exceptionLoggingUsingFinalField() { 1129 | lint() 1130 | .files(TIMBER_STUB, 1131 | java(""" 1132 | |package foo; 1133 | |import timber.log.Timber; 1134 | |public class Example { 1135 | | private final String message = "foo"; 1136 | | public void log() { 1137 | | Exception e = new Exception(); 1138 | | Timber.d(e, message); 1139 | | } 1140 | |}""".trimMargin()), 1141 | kotlin(""" 1142 | |package foo 1143 | |import timber.log.Timber 1144 | |class Example { 1145 | | private val message = "" 1146 | | fun log() { 1147 | | val e = Exception() 1148 | | Timber.d(e, message) 1149 | | } 1150 | |}""".trimMargin()) 1151 | ) 1152 | .issues(*issues) 1153 | .run() 1154 | .expectClean() 1155 | } 1156 | 1157 | @Test fun exceptionLoggingUsingEmptyStringMessage() { 1158 | lint() 1159 | .files(TIMBER_STUB, 1160 | java(""" 1161 | |package foo; 1162 | |import timber.log.Timber; 1163 | |public class Example { 1164 | | public void log() { 1165 | | Exception e = new Exception(); 1166 | | Timber.d(e, ""); 1167 | | } 1168 | |}""".trimMargin()), 1169 | kotlin(""" 1170 | |package foo 1171 | |import timber.log.Timber 1172 | |class Example { 1173 | | fun log() { 1174 | | val e = Exception() 1175 | | Timber.d(e, "") 1176 | | } 1177 | |}""".trimMargin()) 1178 | ) 1179 | .issues(*issues) 1180 | .run() 1181 | .expect(""" 1182 | |src/foo/Example.java:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging] 1183 | | Timber.d(e, ""); 1184 | | ~~~~~~~~~~~~~~~ 1185 | |src/foo/Example.kt:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging] 1186 | | Timber.d(e, "") 1187 | | ~~~~~~~~~~~~~~~ 1188 | |0 errors, 2 warnings""".trimMargin()) 1189 | .expectFixDiffs(""" 1190 | |Fix for src/foo/Example.java line 6: Remove redundant argument: 1191 | |@@ -6 +6 1192 | |- Timber.d(e, ""); 1193 | |+ Timber.d(e); 1194 | |Fix for src/foo/Example.kt line 6: Remove redundant argument: 1195 | |@@ -6 +6 1196 | |- Timber.d(e, "") 1197 | |+ Timber.d(e) 1198 | |""".trimMargin()) 1199 | } 1200 | 1201 | @Test fun exceptionLoggingUsingNullMessage() { 1202 | lint() 1203 | .files(TIMBER_STUB, 1204 | java(""" 1205 | |package foo; 1206 | |import timber.log.Timber; 1207 | |public class Example { 1208 | | public void log() { 1209 | | Exception e = new Exception(); 1210 | | Timber.d(e, null); 1211 | | } 1212 | |}""".trimMargin()), 1213 | kotlin(""" 1214 | |package foo 1215 | |import timber.log.Timber 1216 | |class Example { 1217 | | fun log() { 1218 | | val e = Exception() 1219 | | Timber.d(e, null) 1220 | | } 1221 | |}""".trimMargin()) 1222 | ) 1223 | .issues(*issues) 1224 | .run() 1225 | .expect(""" 1226 | |src/foo/Example.java:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging] 1227 | | Timber.d(e, null); 1228 | | ~~~~~~~~~~~~~~~~~ 1229 | |src/foo/Example.kt:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging] 1230 | | Timber.d(e, null) 1231 | | ~~~~~~~~~~~~~~~~~ 1232 | |0 errors, 2 warnings""".trimMargin()) 1233 | .expectFixDiffs(""" 1234 | |Fix for src/foo/Example.java line 6: Remove redundant argument: 1235 | |@@ -6 +6 1236 | |- Timber.d(e, null); 1237 | |+ Timber.d(e); 1238 | |Fix for src/foo/Example.kt line 6: Remove redundant argument: 1239 | |@@ -6 +6 1240 | |- Timber.d(e, null) 1241 | |+ Timber.d(e) 1242 | |""".trimMargin()) 1243 | } 1244 | 1245 | @Test fun exceptionLoggingUsingValidMessage() { 1246 | lint() 1247 | .files(TIMBER_STUB, 1248 | java(""" 1249 | |package foo; 1250 | |import timber.log.Timber; 1251 | |public class Example { 1252 | | public void log() { 1253 | | Exception e = new Exception(); 1254 | | Timber.d(e, "Valid message"); 1255 | | } 1256 | |}""".trimMargin()), 1257 | kotlin(""" 1258 | |package foo 1259 | |import timber.log.Timber 1260 | |class Example { 1261 | | fun log() { 1262 | | val e = Exception() 1263 | | Timber.d(e, "Valid message") 1264 | | } 1265 | |}""".trimMargin()) 1266 | ) 1267 | .issues(*issues) 1268 | .run() 1269 | .expectClean() 1270 | } 1271 | 1272 | @Test fun dateFormatNotDisplayingWarning() { 1273 | lint() 1274 | .files(TIMBER_STUB, 1275 | java(""" 1276 | |package foo; 1277 | |import timber.log.Timber; 1278 | |public class Example { 1279 | | public void log() { 1280 | | Timber.d("%tc", new java.util.Date()); 1281 | | } 1282 | |}""".trimMargin()), 1283 | kotlin(""" 1284 | |package foo 1285 | |import timber.log.Timber 1286 | |class Example { 1287 | | fun log() { 1288 | | Timber.d("%tc", java.util.Date()) 1289 | | } 1290 | |}""".trimMargin()) 1291 | ) 1292 | .issues(*issues) 1293 | .run() 1294 | .expectClean() 1295 | } 1296 | 1297 | @Test fun systemTimeMillisValidMessage() { 1298 | lint() 1299 | .files(TIMBER_STUB, 1300 | java(""" 1301 | |package foo; 1302 | |import timber.log.Timber; 1303 | |public class Example { 1304 | | public void log() { 1305 | | Timber.d("%tc", System.currentTimeMillis()); 1306 | | } 1307 | |}""".trimMargin()), 1308 | kotlin(""" 1309 | |package foo 1310 | |import timber.log.Timber 1311 | |class Example { 1312 | | fun log() { 1313 | | Timber.d("%tc", System.currentTimeMillis()) 1314 | | } 1315 | |}""".trimMargin()) 1316 | ) 1317 | .issues(*issues) 1318 | .run() 1319 | .expectClean() 1320 | } 1321 | 1322 | @Test fun wrappedBooleanType() { 1323 | lint() 1324 | .files(TIMBER_STUB, 1325 | java(""" 1326 | |package foo; 1327 | |import timber.log.Timber; 1328 | |public class Example { 1329 | | public void log() { 1330 | | Timber.d("%b", Boolean.valueOf(true)); 1331 | | } 1332 | |}""".trimMargin()), 1333 | // no kotlin equivalent, since primitive wrappers do not exist 1334 | ) 1335 | .issues(*issues) 1336 | .run() 1337 | .expectClean() 1338 | } 1339 | 1340 | @Test fun memberVariable() { 1341 | lint() 1342 | .files(TIMBER_STUB, 1343 | java(""" 1344 | |package foo; 1345 | |import timber.log.Timber; 1346 | |public class Example { 1347 | | public static class Bar { 1348 | | public static String baz = "timber"; 1349 | | } 1350 | | public void log() { 1351 | | Bar bar = new Bar(); 1352 | | Timber.d(bar.baz); 1353 | | } 1354 | |} 1355 | """.trimMargin()), 1356 | kotlin(""" 1357 | |package foo 1358 | |import timber.log.Timber 1359 | |class Example { 1360 | | class Bar { 1361 | | val baz = "timber" 1362 | | } 1363 | | fun log() { 1364 | | val bar = Bar() 1365 | | Timber.d(bar.baz) 1366 | | } 1367 | |} 1368 | """.trimMargin()) 1369 | ) 1370 | .issues(*issues) 1371 | .run() 1372 | .expectClean() 1373 | } 1374 | } 1375 | -------------------------------------------------------------------------------- /timber-sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion versions.compileSdk 5 | 6 | buildFeatures { 7 | buildConfig = true 8 | viewBinding = true 9 | } 10 | 11 | defaultConfig { 12 | applicationId 'com.example.timber' 13 | minSdkVersion versions.minSdk 14 | targetSdkVersion versions.compileSdk 15 | versionCode 1 16 | versionName '1.0.0' 17 | } 18 | 19 | lintOptions { 20 | textReport true 21 | textOutput 'stdout' 22 | ignore 'InvalidPackage' 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation project(':timber') 28 | } 29 | -------------------------------------------------------------------------------- /timber-sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /timber-sample/src/main/java/com/example/timber/ExampleApp.java: -------------------------------------------------------------------------------- 1 | package com.example.timber; 2 | 3 | import static timber.log.Timber.DebugTree; 4 | 5 | import android.app.Application; 6 | import android.util.Log; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import timber.log.Timber; 11 | 12 | public class ExampleApp extends Application { 13 | @Override public void onCreate() { 14 | super.onCreate(); 15 | 16 | if (BuildConfig.DEBUG) { 17 | Timber.plant(new DebugTree()); 18 | } else { 19 | Timber.plant(new CrashReportingTree()); 20 | } 21 | } 22 | 23 | /** A tree which logs important information for crash reporting. */ 24 | private static class CrashReportingTree extends Timber.Tree { 25 | @Override protected void log(int priority, String tag, @NonNull String message, Throwable t) { 26 | if (priority == Log.VERBOSE || priority == Log.DEBUG) { 27 | return; 28 | } 29 | 30 | FakeCrashLibrary.log(priority, tag, message); 31 | 32 | if (t != null) { 33 | if (priority == Log.ERROR) { 34 | FakeCrashLibrary.logError(t); 35 | } else if (priority == Log.WARN) { 36 | FakeCrashLibrary.logWarning(t); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /timber-sample/src/main/java/com/example/timber/FakeCrashLibrary.java: -------------------------------------------------------------------------------- 1 | package com.example.timber; 2 | 3 | /** Not a real crash reporting library! */ 4 | public final class FakeCrashLibrary { 5 | public static void log(int priority, String tag, String message) { 6 | // TODO add log entry to circular buffer. 7 | } 8 | 9 | public static void logWarning(Throwable t) { 10 | // TODO report non-fatal warning. 11 | } 12 | 13 | public static void logError(Throwable t) { 14 | // TODO report non-fatal error. 15 | } 16 | 17 | private FakeCrashLibrary() { 18 | throw new AssertionError("No instances."); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /timber-sample/src/main/java/com/example/timber/ui/DemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.timber.ui; 2 | 3 | import static android.widget.Toast.LENGTH_SHORT; 4 | 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.Toast; 10 | 11 | import com.example.timber.databinding.DemoActivityBinding; 12 | 13 | import timber.log.Timber; 14 | 15 | public class DemoActivity extends Activity implements View.OnClickListener { 16 | @Override protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | DemoActivityBinding binding = DemoActivityBinding.inflate(getLayoutInflater()); 19 | setContentView(binding.getRoot()); 20 | 21 | Timber.tag("LifeCycles"); 22 | Timber.d("Activity Created"); 23 | 24 | binding.hello.setOnClickListener(this); 25 | binding.hey.setOnClickListener(this); 26 | binding.hi.setOnClickListener(this); 27 | } 28 | 29 | @Override public void onClick(View v) { 30 | Button button = (Button) v; 31 | Timber.i("A button with ID %s was clicked to say '%s'.", button.getId(), button.getText()); 32 | Toast.makeText(this, "Check logcat for a greeting!", LENGTH_SHORT).show(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /timber-sample/src/main/java/com/example/timber/ui/JavaLintActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.timber.ui; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import androidx.annotation.Nullable; 8 | import timber.log.Timber; 9 | 10 | import static java.lang.String.format; 11 | 12 | @SuppressLint("Registered") // 13 | public class JavaLintActivity extends Activity { 14 | /** 15 | * Below are some examples of how NOT to use Timber. 16 | * 17 | * To see how a particular lint issue behaves, comment/remove its corresponding id from the set 18 | * of SuppressLint ids below. 19 | */ 20 | @SuppressLint({ 21 | "LogNotTimber", // 22 | "StringFormatInTimber", // 23 | "ThrowableNotAtBeginning", // 24 | "BinaryOperationInTimber", // 25 | "TimberArgCount", // 26 | "TimberArgTypes", // 27 | "TimberTagLength", // 28 | "TimberExceptionLogging" // 29 | }) // 30 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | 33 | // LogNotTimber 34 | Log.d("TAG", "msg"); 35 | Log.d("TAG", "msg", new Exception()); 36 | android.util.Log.d("TAG", "msg"); 37 | android.util.Log.d("TAG", "msg", new Exception()); 38 | 39 | // StringFormatInTimber 40 | Timber.w(String.format("%s", getString())); 41 | Timber.w(format("%s", getString())); 42 | 43 | // ThrowableNotAtBeginning 44 | Timber.d("%s", new Exception()); 45 | 46 | // BinaryOperationInTimber 47 | String foo = "foo"; 48 | String bar = "bar"; 49 | Timber.d("foo" + "bar"); 50 | Timber.d("foo" + bar); 51 | Timber.d(foo + "bar"); 52 | Timber.d(foo + bar); 53 | 54 | // TimberArgCount 55 | Timber.d("%s %s", "arg0"); 56 | Timber.d("%s", "arg0", "arg1"); 57 | Timber.tag("tag").d("%s %s", "arg0"); 58 | Timber.tag("tag").d("%s", "arg0", "arg1"); 59 | 60 | // TimberArgTypes 61 | Timber.d("%d", "arg0"); 62 | Timber.tag("tag").d("%d", "arg0"); 63 | 64 | // TimberTagLength 65 | Timber.tag("abcdefghijklmnopqrstuvwx"); 66 | Timber.tag("abcdefghijklmnopqrstuvw" + "x"); 67 | 68 | // TimberExceptionLogging 69 | Timber.d(new Exception(), new Exception().getMessage()); 70 | Timber.d(new Exception(), ""); 71 | Timber.d(new Exception(), null); 72 | Timber.d(new Exception().getMessage()); 73 | } 74 | 75 | private String getString() { 76 | return "foo"; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /timber-sample/src/main/java/com/example/timber/ui/KotlinLintActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.timber.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.os.Bundle 6 | import android.util.Log 7 | import timber.log.Timber 8 | import java.lang.Exception 9 | import java.lang.String.format 10 | 11 | @SuppressLint("Registered") 12 | class KotlinLintActivity : Activity() { 13 | /** 14 | * Below are some examples of how NOT to use Timber. 15 | * 16 | * To see how a particular lint issue behaves, comment/remove its corresponding id from the set 17 | * of SuppressLint ids below. 18 | */ 19 | @SuppressLint( 20 | "LogNotTimber", 21 | "StringFormatInTimber", 22 | "ThrowableNotAtBeginning", 23 | "BinaryOperationInTimber", 24 | "TimberArgCount", 25 | "TimberArgTypes", 26 | "TimberTagLength", 27 | "TimberExceptionLogging" 28 | ) 29 | @Suppress("RemoveRedundantQualifierName") 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | // LogNotTimber 34 | Log.d("TAG", "msg") 35 | Log.d("TAG", "msg", Exception()) 36 | android.util.Log.d("TAG", "msg") 37 | android.util.Log.d("TAG", "msg", Exception()) 38 | 39 | // StringFormatInTimber 40 | Timber.w(String.format("%s", getString())) 41 | Timber.w(format("%s", getString())) 42 | 43 | // ThrowableNotAtBeginning 44 | Timber.d("%s", Exception()) 45 | 46 | // BinaryOperationInTimber 47 | val foo = "foo" 48 | val bar = "bar" 49 | Timber.d("foo" + "bar") 50 | Timber.d("foo$bar") 51 | Timber.d("${foo}bar") 52 | Timber.d("$foo$bar") 53 | 54 | // TimberArgCount 55 | Timber.d("%s %s", "arg0") 56 | Timber.d("%s", "arg0", "arg1") 57 | Timber.tag("tag").d("%s %s", "arg0") 58 | Timber.tag("tag").d("%s", "arg0", "arg1") 59 | 60 | // TimberArgTypes 61 | Timber.d("%d", "arg0") 62 | Timber.tag("tag").d("%d", "arg0") 63 | 64 | // TimberTagLength 65 | Timber.tag("abcdefghijklmnopqrstuvwx") 66 | Timber.tag("abcdefghijklmnopqrstuvw" + "x") 67 | 68 | // TimberExceptionLogging 69 | Timber.d(Exception(), Exception().message) 70 | Timber.d(Exception(), "") 71 | Timber.d(Exception(), null) 72 | Timber.d(Exception().message) 73 | } 74 | 75 | private fun getString() = "foo" 76 | } -------------------------------------------------------------------------------- /timber-sample/src/main/res/layout/demo_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 |