[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline=true\nend_of_line=lf\ncharset=utf-8\nindent_size=2\ntrim_trailing_whitespace=true\n\n[*.{kt,kts}]\nij_kotlin_allow_trailing_comma=true\nij_kotlin_allow_trailing_comma_on_call_site=true\nij_kotlin_imports_layout=*\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "{\n\t$schema: 'https://docs.renovatebot.com/renovate-schema.json',\n\textends: [\n\t\t'config:recommended',\n\t],\n\tpackageRules: [\n\t\t{\n\t\t\tgroupName: 'AGP',\n\t\t\tmatchPackagePatterns: [\n\t\t\t\t'com.android.tools.*',\n\t\t\t],\n\t\t},\n\t],\n\tignorePresets: [\n\t\t// Ensure we get the latest version and are not pinned to old versions.\n\t\t'workarounds:javaLTSVersions',\n\t],\n\tcustomManagers: [\n\t\t// Update .java-version file with the latest JDK version.\n\t\t{\n\t\t\tcustomType: 'regex',\n\t\t\tfileMatch: [\n\t\t\t\t'\\\\.java-version$',\n\t\t\t],\n\t\t\tmatchStrings: [\n\t\t\t\t'(?<currentValue>.*)\\\\n',\n\t\t\t],\n\t\t\tdatasourceTemplate: 'java-version',\n\t\t\tdepNameTemplate: 'java',\n\t\t\t// Only write the major version.\n\t\t\textractVersionTemplate: '^(?<version>\\\\d+)',\n\t\t},\n\t],\n}\n"
  },
  {
    "path": ".github/workflows/.java-version",
    "content": "25\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: build\n\non:\n  pull_request: {}\n  workflow_dispatch: {}\n  push:\n    branches:\n      - 'trunk'\n    tags-ignore:\n      - '**'\n\nenv:\n  GRADLE_OPTS: \"-Dorg.gradle.daemon=false -Dorg.gradle.vfs.watch=false -Dkotlin.incremental=false  -Dorg.gradle.logging.stacktrace=full\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version-file: .github/workflows/.java-version\n      - uses: gradle/actions/setup-gradle@v5\n\n      - run: ./gradlew build dokkaGenerate\n\n      - run: ./gradlew publish\n        if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/timber' }}\n        env:\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }}\n\n      - name: Deploy docs to website\n        if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/timber' }}\n        uses: JamesIves/github-pages-deploy-action@releases/v3\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BRANCH: site\n          FOLDER: timber/build/dokka/html/\n          TARGET_FOLDER: docs/latest/\n          CLEAN: true\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: release\n\non:\n  push:\n    tags:\n      - '**'\n\nenv:\n  GRADLE_OPTS: \"-Dorg.gradle.daemon=false -Dorg.gradle.vfs.watch=false -Dkotlin.incremental=false  -Dorg.gradle.logging.stacktrace=full\"\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version-file: .github/workflows/.java-version\n      - uses: gradle/actions/setup-gradle@v5\n\n      - run: ./gradlew -p mosaic publish\n        env:\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }}\n\n      - name: Extract release notes\n        id: release_notes\n        uses: ffurrer2/extract-release-notes@v3\n\n      - name: Create release\n        uses: softprops/action-gh-release@v2\n        with:\n          body: ${{ steps.release_notes.outputs.release_notes }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - run: ./gradlew dokkaGenerate\n\n      - name: Deploy docs to website\n        uses: JamesIves/github-pages-deploy-action@releases/v3\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BRANCH: site\n          FOLDER: timber/build/dokka/html/\n          TARGET_FOLDER: docs/5.x/\n          CLEAN: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".classpath\n.project\n.settings\neclipsebin\n\nbin\ngen\nbuild\nout\nlib\n\n.idea\n*.iml\nclasses\n\nobj\n\n.DS_Store\n\n# Gradle\n.gradle\njniLibs\nbuild\nlocal.properties\nreports\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change log\n\n## [Unreleased]\n\n\n## [5.0.1] - 2021-08-13\n\n### Fixed\n\n- Fix TimberArgCount lint check false positive on some calls to `String.format`.\n\n\n## [5.0.0] - 2021-08-10\n\nThe library has been rewritten in Kotlin, but it remains binary-compatible with 4.x.\nThe intent is to support Kotlin multiplatform in the future.\nThis is otherwise a relatively minor, bug-fix release.\n\n### Changed\n\n- Minimum supported API level is now 14.\n- Minimum supported AGP (for embedded lint checks) is now 7.0.\n\n### Fixed\n\n- `DebugTree` now finds first non-library class name which prevents exceptions in optimized builds where expected stackframes may have been inlined.\n- Enforce 23-character truncated tag length until API 26 per AOSP sources.\n- Support `Long` type for date/time format arguments when validating format strings in lint checks.\n- Do not report string literal concatenation in lint checks on log message.\n\n\n## [4.7.1] - 2018-06-28\n\n * Fix: Redundant argument lint check now works correctly on Kotlin sources.\n\n\n## [4.7.0] - 2018-03-27\n\n * Fix: Support lint version 26.1.0.\n * Fix: Check single-argument log method in TimberExceptionLogging.\n\n\n## [4.6.1] - 2018-02-12\n\n * Fix: Lint checks now handle more edge cases around exception and message source.\n * Fix: Useless `BuildConfig` class is no longer included.\n\n\n## [4.6.0] - 2017-10-30\n\n * 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.\n * New: Added nullability annotations for Kotlin users.\n * Fix: Tag length truncation no longer occurs on API 24 or newer as the system no longer has a length restriction.\n * Fix: Handle when a `null` array is supplied for the message arguments. This can occur when using various bytecode optimization tools.\n\n\n## [4.5.1] - 2017-01-20\n\n * Fix: String formatting lint check now correctly works with dates.\n\n\n## [4.5.0] - 2017-01-09\n\n * New: Automatically truncate class name tags to Android's limit of 23 characters.\n * New: Lint check for detecting null/empty messages or using the exception message when logging an\n   exception. Use the single-argument logging overloads instead.\n * Fix: Correct NPE in lint check when using String.format.\n\n\n## [4.4.0] - 2016-12-06\n\n * New: `Tree.formatMessage` method allows customization of message formatting and rendering.\n * New: Lint checks ported to new IntelliJ PSI infrastructure.\n\n\n## [4.3.1] - 2016-09-19\n\n * New: Add `isLoggable` convenience method which also provides the tag.\n\n\n## [4.3.0] - 2016-08-18\n\n * New: Overloads for all log methods which accept only a `Throwable` without a message.\n\n\n## [4.2.0] - 2016-08-12\n\n * New: `Timber.plant` now has a varargs overload for planting multiple trees at once.\n * New: minSdkVersion is now 9 because reasons.\n * Fix: Consume explicitly specified tag even if the message is determined as not loggable (due to level).\n * Fix: Allow lint checks to run when `Timber.tag(..).v(..)`-style logging is used.\n\n\n## [4.1.2] - 2016-03-30\n\n * Fix: Tag-length lint check now only triggers on calls to `Timber`'s `tag` method. Previously it would\n   match _any_ `tag` method and flag arguments longer than 23 characters.\n\n\n## [4.1.1] - 2016-02-19\n\n * New: Add method for retreiving the number of installed trees.\n\n\n## [4.1.0] - 2015-10-19\n\n * New: Consumer ProGuard rule automatically suppresses a warning for the use `@NonNls` on the 'message'\n   argument for logging method. The warning was only for users running ProGuard and can safely be ignored.\n * New: Static `log` methods which accept a priority as a first argument makes dynamic logging at different\n   levels easier to support.\n * Fix: Replace internal use of `Log.getStackTraceString` with our own implementation. This ensures that\n   `UnknownHostException` errors are logged, which previously were suppressed.\n * Fix: 'BinaryOperationInTimber' lint rule now only triggers for string concatenation.\n\n\n## [4.0.1] - 2015-10-07\n\n * Fix: TimberArgTypes lint rule now allows booleans and numbers in '%s' format markers.\n * Fix: Lint rules now support running on Java 7 VMs.\n\n\n## [4.0.0] - 2015-10-07\n\n * New: Library is now an .aar! This means the lint rules are automatically applied to consuming\n   projects.\n * New: `Tree.forest()` returns an immutable copy of all planted trees.\n * Fix: Ensure thread safety when logging and adding or removing trees concurrently.\n\n\n## [3.1.0] - 2015-05-11\n\n * New: `Tree.isLoggable` method allows a tree to determine whether a statement should be logged\n   based on its priority. Defaults to logging all levels.\n\n\n## [3.0.2] - 2015-05-01\n\n * Fix: Strip multiple anonymous class markers (e.g., `$1$2`) from class names when `DebugTree`\n   is creating an inferred tag.\n\n\n## [3.0.1] - 2015-04-17\n\n * Fix: String formatting is now always applied when arguments are present. Previously it would\n   only trigger when an exception was included.\n\n\n## [3.0.0] - 2015-04-16\n\n * New: `Tree` and `DebugTree` APIs are much more extensible requiring only a single method to\n   override.\n * New: `DebugTree` exposes `createStackElementTag` method for overriding to customize the\n   reflection-based tag creation (for example, such as to add a line number).\n * WTF: Support for `wtf` log level.\n * `HollowTree` has been removed as it is no longer needed. Just extend `Tree`.\n * `TaggedTree` has been removed and its functionality folded into `Tree`. All `Tree` instances\n   will receive any tags specified by a call to `tag`.\n * Fix: Multiple planted `DebugTree`s now each correctly received tags set from a call to `tag`.\n\n\n## [2.7.1] - 2015-02-17\n\n * Fix: Switch method of getting calling class to be consistent across API levels.\n\n\n## [2.7.0] - 2015-02-17\n\n * New: `DebugTree` subclasses can now override `logMessage` for access to the priority, tag, and\n   entire message for every log.\n * Fix: Prevent overriding `Tree` and `TaggedTree` methods on `DebugTree`.\n\n\n## [2.6.0] - 2015-02-17\n\n * New: `DebugTree` subclasses can now override `createTag()` to specify log tags. `nextTag()` is\n   also accessible for querying if an explicit tag was set.\n\n\n## [2.5.1] - 2015-01-19\n\n * Fix: Properly split lines which contain both newlines and are over 4000 characters.\n * Explicitly forbid `null` tree instances.\n\n\n## [2.5.0] - 2014-11-08\n\n * New: `Timber.asTree()` exposes functionality as a `Tree` instance rather than static methods.\n\n\n## [2.4.2] - 2014-11-07\n\n * Eliminate heap allocation when dispatching log calls.\n\n\n## [2.4.1] - 2014-06-19\n\n * Fix: Calls with no message but a `Throwable` are now correctly logged.\n\n\n## [2.4.0] - 2014-06-10\n\n * New: `uproot` and `uprootAll` methods allow removing trees.\n\n\n## [2.3.0] - 2014-05-21\n\n * New: Messages longer than 4000 characters will be split into multiple lines.\n\n\n## [2.2.2] - 2014-02-12\n\n * Fix: Include debug level in previous fix which avoids formatting messages with no arguments.\n\n\n## [2.2.1] - 2014-02-11\n\n * Fix: Do not attempt to format log messages which do not have arguments.\n\n\n## [2.2.0] - 2014-02-02\n\n * New: verbose log level added (`v()`).\n * New: `timber-lint` module adds lint check to ensure you are calling `Timber` and not `Log`.\n * Fix: Specifying custom tags is now thread-safe.\n\n\n## [2.1.0] - 2013-11-21\n\n * New: `tag` method allows specifying custom one-time tag. Redux!\n\n\n## [2.0.0] - 2013-10-21\n\n * Logging API is now exposed as static methods on `Timber`. Behavior is added by installing `Tree`\n   instances for logging.\n\n\n## [1.1.0] - 2013-07-22\n\n * New: `tag` method allows specifying custom one-time tag.\n * Fix: Exception-containing methods now log at the correct level.\n\n\n## [1.0.0] - 2013-07-17\n\nInitial cut. (Get it?)\n\n\n\n\n[Unreleased]: https://github.com/JakeWharton/timber/compare/5.0.1...HEAD\n[5.0.1]: https://github.com/JakeWharton/timber/releases/tag/5.0.1\n[5.0.0]: https://github.com/JakeWharton/timber/releases/tag/5.0.0\n[4.7.1]: https://github.com/JakeWharton/timber/releases/tag/4.7.1\n[4.7.0]: https://github.com/JakeWharton/timber/releases/tag/4.7.0\n[4.6.1]: https://github.com/JakeWharton/timber/releases/tag/4.6.1\n[4.6.0]: https://github.com/JakeWharton/timber/releases/tag/4.6.0\n[4.5.1]: https://github.com/JakeWharton/timber/releases/tag/4.5.1\n[4.5.0]: https://github.com/JakeWharton/timber/releases/tag/4.5.0\n[4.4.0]: https://github.com/JakeWharton/timber/releases/tag/4.4.0\n[4.3.1]: https://github.com/JakeWharton/timber/releases/tag/4.3.1\n[4.3.0]: https://github.com/JakeWharton/timber/releases/tag/4.3.0\n[4.2.0]: https://github.com/JakeWharton/timber/releases/tag/4.2.0\n[4.1.2]: https://github.com/JakeWharton/timber/releases/tag/4.1.2\n[4.1.1]: https://github.com/JakeWharton/timber/releases/tag/4.1.1\n[4.1.0]: https://github.com/JakeWharton/timber/releases/tag/4.1.0\n[4.0.1]: https://github.com/JakeWharton/timber/releases/tag/4.0.1\n[4.0.0]: https://github.com/JakeWharton/timber/releases/tag/4.0.0\n[3.1.0]: https://github.com/JakeWharton/timber/releases/tag/3.1.0\n[3.0.2]: https://github.com/JakeWharton/timber/releases/tag/3.0.2\n[3.0.1]: https://github.com/JakeWharton/timber/releases/tag/3.0.1\n[3.0.0]: https://github.com/JakeWharton/timber/releases/tag/3.0.0\n[2.7.1]: https://github.com/JakeWharton/timber/releases/tag/2.7.1\n[2.7.0]: https://github.com/JakeWharton/timber/releases/tag/2.7.0\n[2.6.0]: https://github.com/JakeWharton/timber/releases/tag/2.6.0\n[2.5.1]: https://github.com/JakeWharton/timber/releases/tag/2.5.1\n[2.5.0]: https://github.com/JakeWharton/timber/releases/tag/2.5.0\n[2.4.2]: https://github.com/JakeWharton/timber/releases/tag/2.4.2\n[2.4.1]: https://github.com/JakeWharton/timber/releases/tag/2.4.1\n[2.4.0]: https://github.com/JakeWharton/timber/releases/tag/2.4.0\n[2.3.0]: https://github.com/JakeWharton/timber/releases/tag/2.3.0\n[2.2.2]: https://github.com/JakeWharton/timber/releases/tag/2.2.2\n[2.2.1]: https://github.com/JakeWharton/timber/releases/tag/2.2.1\n[2.2.0]: https://github.com/JakeWharton/timber/releases/tag/2.2.0\n[2.1.0]: https://github.com/JakeWharton/timber/releases/tag/2.1.0\n[2.0.0]: https://github.com/JakeWharton/timber/releases/tag/2.0.0\n[1.1.0]: https://github.com/JakeWharton/timber/releases/tag/1.1.0\n[1.0.0]: https://github.com/JakeWharton/timber/releases/tag/1.0.0\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "![Timber](logo.png)\n\nThis is a logger with a small, extensible API which provides utility on top of Android's normal\n`Log` class.\n\nI copy this class into all the little apps I make. I'm tired of doing it. Now it's a library.\n\nBehavior is added through `Tree` instances. You can install an instance by calling `Timber.plant`.\nInstallation of `Tree`s should be done as early as possible. The `onCreate` of your application is\nthe most logical choice.\n\nThe `DebugTree` implementation will automatically figure out from which class it's being called and\nuse that class name as its tag. Since the tags vary, it works really well when coupled with a log\nreader like [Pidcat][1].\n\nThere are no `Tree` implementations installed by default because every time you log in production, a\npuppy dies.\n\n\nUsage\n-----\n\nTwo easy steps:\n\n 1. Install any `Tree` instances you want in the `onCreate` of your application class.\n 2. Call `Timber`'s static methods everywhere throughout your app.\n\nCheck out the sample app in `timber-sample/` to see it in action.\n\n\nLint\n----\n\nTimber ships with embedded lint rules to detect problems in your app.\n\n *  **TimberArgCount** (Error) - Detects an incorrect number of arguments passed to a `Timber` call for\n    the specified format string.\n\n        Example.java:35: Error: Wrong argument count, format string Hello %s %s! requires 2 but format call supplies 1 [TimberArgCount]\n            Timber.d(\"Hello %s %s!\", firstName);\n            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n *  **TimberArgTypes** (Error) - Detects arguments which are of the wrong type for the specified format string.\n\n        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]\n            Timber.d(\"success = %b\", taskName);\n                                     ~~~~~~~~\n *  **TimberTagLength** (Error) - Detects the use of tags which are longer than Android's maximum length of 23.\n\n        Example.java:35: Error: The logging tag can be at most 23 characters, was 35 (TagNameThatIsReallyReallyReallyLong) [TimberTagLength]\n            Timber.tag(\"TagNameThatIsReallyReallyReallyLong\").d(\"Hello %s %s!\", firstName, lastName);\n            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n *  **LogNotTimber** (Warning) - Detects usages of Android's `Log` that should be using `Timber`.\n\n        Example.java:35: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n            Log.d(\"Greeting\", \"Hello \" + firstName + \" \" + lastName + \"!\");\n                ~\n\n *  **StringFormatInTimber** (Warning) - Detects `String.format` used inside of a `Timber` call. Timber\n    handles string formatting automatically.\n\n        Example.java:35: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n            Timber.d(String.format(\"Hello, %s %s\", firstName, lastName));\n                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n *  **BinaryOperationInTimber** (Warning) - Detects string concatenation inside of a `Timber` call. Timber\n    handles string formatting automatically and should be preferred over manual concatenation.\n\n        Example.java:35: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber]\n            Timber.d(\"Hello \" + firstName + \" \" + lastName + \"!\");\n                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n *  **TimberExceptionLogging** (Warning) - Detects the use of null or empty messages, or using the exception message\n    when logging an exception.\n\n        Example.java:35: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging]\n             Timber.d(e, e.getMessage());\n                         ~~~~~~~~~~~~~~\n\n\nDownload\n--------\n\n```groovy\nrepositories {\n  mavenCentral()\n}\n\ndependencies {\n  implementation 'com.jakewharton.timber:timber:5.0.1'\n}\n```\n\nDocumentation is available at [jakewharton.github.io/timber/docs/5.x/](https://jakewharton.github.io/timber/docs/5.x/).\n\n<details>\n<summary>Snapshots of the development version are available in Sonatype's snapshots repository.</summary>\n<p>\n\n```groovy\nrepositories {\n  mavenCentral()\n  maven {\n    url 'https://central.sonatype.com/repository/maven-snapshots/'\n  }\n}\n\ndependencies {\n  implementation 'com.jakewharton.timber:timber:5.1.0-SNAPSHOT'\n}\n```\n\nSnapshot documentation is available at [jakewharton.github.io/timber/docs/latest/](https://jakewharton.github.io/timber/docs/latest/).\n\n</p>\n</details>\n\n\nLicense\n-------\n\n    Copyright 2013 Jake Wharton\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n\n\n\n [1]: http://github.com/JakeWharton/pidcat/\n [snap]: https://oss.sonatype.org/content/repositories/snapshots/\n"
  },
  {
    "path": "RELEASING.md",
    "content": "# Releasing\n\n1. Update the `VERSION_NAME` in `gradle.properties` to the release version.\n\n2. Update the `CHANGELOG.md`:\n   1. Change the `Unreleased` header to the release version.\n   2. Add a link URL to ensure the header link works.\n   3. Add a new `Unreleased` section to the top.\n\n3. Update the `README.md`:\n   1. Change the \"Download\" section to reflect the new release version.\n   2. Change the snapshot section to reflect the next \"SNAPSHOT\" version, if it is changing.\n   3. Update the Kotlin version compatibility table\n\n4. Commit\n\n   ```\n   $ git commit -am \"Prepare version X.Y.X\"\n   ```\n\n5. Manually release and upload artifacts\n   1. Run `./gradlew clean publish`\n   2. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.\n   3. If either fails, drop the Sonatype repo, fix the problem, commit, and restart this section.\n\n6. Tag\n\n   ```\n   $ git tag -am \"Version X.Y.Z\" X.Y.Z\n   ```\n\n7. Update the `VERSION_NAME` in `gradle.properties` to the next \"SNAPSHOT\" version.\n\n8. Commit\n\n   ```\n   $ git commit -am \"Prepare next development version\"\n   ```\n\n9. Push!\n\n   ```\n   $ git push && git push --tags\n   ```\n\n   This will trigger a GitHub Action workflow which will create a GitHub release.\n"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n  repositories {\n    mavenCentral()\n    google()\n    gradlePluginPortal()\n  }\n\n  dependencies {\n    classpath libs.gradlePlugin.android\n    classpath libs.gradlePlugin.kotlin\n    classpath libs.gradlePlugin.japicmp\n    classpath libs.gradlePlugin.mavenPublish\n    classpath libs.gradlePlugin.dokka\n    classpath libs.compatPlugin\n    classpath libs.spotlessPlugin\n  }\n}\n\nsubprojects {\n  repositories {\n    mavenCentral()\n    google()\n  }\n\n  apply plugin: 'com.diffplug.spotless'\n  spotless {\n    kotlin {\n      ktfmt(libs.ktfmt.get().version).googleStyle()\n      target('**/*.kt')\n    }\n  }\n\n  tasks.withType(Test) {\n    testLogging {\n      events \"failed\"\n      exceptionFormat \"full\"\n      showExceptions true\n      showStackTraces true\n      showCauses true\n    }\n  }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"9.1.0\"\nandroidTools = \"32.1.0\" # Update this values in sync with agp.\nkotlin = \"2.3.20\"\nautoService = \"1.1.1\"\nminSdk = \"14\"\ncompileSdk = \"36\"\n\n[libraries]\ngradlePlugin-android = { module = \"com.android.tools.build:gradle\", version.ref = \"agp\" }\ngradlePlugin-kotlin = { module = \"org.jetbrains.kotlin:kotlin-gradle-plugin\", version.ref = \"kotlin\" }\ngradlePlugin-dokka = \"org.jetbrains.dokka:dokka-gradle-plugin:2.1.0\"\ngradlePlugin-japicmp = \"me.champeau.gradle:japicmp-gradle-plugin:0.4.6\"\ngradlePlugin-mavenPublish = \"com.vanniktech:gradle-maven-publish-plugin:0.36.0\"\ncompatPlugin = \"com.gradleup.tapmoc:tapmoc-gradle-plugin:0.4.0\"\n\nannotations = \"org.jetbrains:annotations:26.1.0\"\nauto-service = { module = \"com.google.auto.service:auto-service\", version.ref = \"autoService\" }\nauto-annotations = { module = \"com.google.auto.service:auto-service-annotations\", version.ref = \"autoService\" }\n\nlint-core = { module = \"com.android.tools.lint:lint\", version.ref = \"androidTools\" }\nlint-api = { module = \"com.android.tools.lint:lint-api\", version.ref = \"androidTools\" }\nlint-checks = { module = \"com.android.tools.lint:lint-checks\", version.ref = \"androidTools\" }\nlint-tests = { module = \"com.android.tools.lint:lint-tests\", version.ref = \"androidTools\" }\n\njunit = \"junit:junit:4.13.2\"\nassertk = \"com.willowtreeapps.assertk:assertk:0.28.1\"\nrobolectric = \"org.robolectric:robolectric:4.16.1\"\n\nspotlessPlugin = \"com.diffplug.spotless:spotless-plugin-gradle:8.4.0\"\nktfmt = \"com.facebook:ktfmt:0.62\"\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "GROUP=com.jakewharton.timber\n\n# HEY! If you change the major version here be sure to update release.yaml doc target folder!\nVERSION_NAME=5.1.0-SNAPSHOT\n\nPOM_DESCRIPTION=No-nonsense injectable logging.\n\nPOM_URL=https://github.com/JakeWharton/timber\nPOM_SCM_URL=https://github.com/JakeWharton/timber\nPOM_SCM_CONNECTION=scm:git:git://github.com/JakeWharton/timber.git\nPOM_SCM_DEV_CONNECTION=scm:git:git://github.com/JakeWharton/timber.git\n\nPOM_LICENCE_NAME=The Apache Software License, Version 2.0\nPOM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt\nPOM_LICENCE_DIST=repo\n\nPOM_DEVELOPER_ID=jakewharton\nPOM_DEVELOPER_NAME=Jake Wharton\n\norg.gradle.jvmargs=-Xmx1536M\n\nmavenCentralPublishing=true\nmavenCentralAutomaticPublishing=true\nsignAllPublications=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':timber'\ninclude ':timber-lint'\ninclude ':timber-sample'\n\nrootProject.name = 'timber-root'\n"
  },
  {
    "path": "timber/api/timber.api",
    "content": "public final class timber/log/Timber {\n\tpublic static final field Forest Ltimber/log/Timber$Forest;\n\tpublic static fun asTree ()Ltimber/log/Timber$Tree;\n\tpublic static fun d (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun d (Ljava/lang/Throwable;)V\n\tpublic static fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun e (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun e (Ljava/lang/Throwable;)V\n\tpublic static fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static final fun forest ()Ljava/util/List;\n\tpublic static fun i (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun i (Ljava/lang/Throwable;)V\n\tpublic static fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun log (ILjava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun log (ILjava/lang/Throwable;)V\n\tpublic static fun log (ILjava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static final fun plant (Ltimber/log/Timber$Tree;)V\n\tpublic static final fun plant ([Ltimber/log/Timber$Tree;)V\n\tpublic static final fun tag (Ljava/lang/String;)Ltimber/log/Timber$Tree;\n\tpublic static final fun treeCount ()I\n\tpublic static final fun uproot (Ltimber/log/Timber$Tree;)V\n\tpublic static final fun uprootAll ()V\n\tpublic static fun v (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun v (Ljava/lang/Throwable;)V\n\tpublic static fun v (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun w (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun w (Ljava/lang/Throwable;)V\n\tpublic static fun w (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun wtf (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic static fun wtf (Ljava/lang/Throwable;)V\n\tpublic static fun wtf (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n}\n\npublic class timber/log/Timber$DebugTree : timber/log/Timber$Tree {\n\tpublic static final field Companion Ltimber/log/Timber$DebugTree$Companion;\n\tpublic fun <init> ()V\n\tprotected fun createStackElementTag (Ljava/lang/StackTraceElement;)Ljava/lang/String;\n\tprotected fun log (ILjava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V\n}\n\npublic final class timber/log/Timber$DebugTree$Companion {\n}\n\npublic final class timber/log/Timber$Forest : timber/log/Timber$Tree {\n\tpublic fun asTree ()Ltimber/log/Timber$Tree;\n\tpublic fun d (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun d (Ljava/lang/Throwable;)V\n\tpublic fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun e (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun e (Ljava/lang/Throwable;)V\n\tpublic fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic final fun forest ()Ljava/util/List;\n\tpublic fun i (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun i (Ljava/lang/Throwable;)V\n\tpublic fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun log (ILjava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun log (ILjava/lang/Throwable;)V\n\tpublic fun log (ILjava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic final fun plant (Ltimber/log/Timber$Tree;)V\n\tpublic final fun plant ([Ltimber/log/Timber$Tree;)V\n\tpublic final fun tag (Ljava/lang/String;)Ltimber/log/Timber$Tree;\n\tpublic final fun treeCount ()I\n\tpublic final fun uproot (Ltimber/log/Timber$Tree;)V\n\tpublic final fun uprootAll ()V\n\tpublic fun v (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun v (Ljava/lang/Throwable;)V\n\tpublic fun v (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun w (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun w (Ljava/lang/Throwable;)V\n\tpublic fun w (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun wtf (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun wtf (Ljava/lang/Throwable;)V\n\tpublic fun wtf (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n}\n\npublic abstract class timber/log/Timber$Tree {\n\tpublic fun <init> ()V\n\tpublic fun d (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun d (Ljava/lang/Throwable;)V\n\tpublic fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun e (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun e (Ljava/lang/Throwable;)V\n\tpublic fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tprotected fun formatMessage (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;\n\tpublic fun i (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun i (Ljava/lang/Throwable;)V\n\tpublic fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tprotected fun isLoggable (I)Z\n\tprotected fun isLoggable (Ljava/lang/String;I)Z\n\tprotected abstract fun log (ILjava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V\n\tpublic fun log (ILjava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun log (ILjava/lang/Throwable;)V\n\tpublic fun log (ILjava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun v (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun v (Ljava/lang/Throwable;)V\n\tpublic fun v (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun w (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun w (Ljava/lang/Throwable;)V\n\tpublic fun w (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun wtf (Ljava/lang/String;[Ljava/lang/Object;)V\n\tpublic fun wtf (Ljava/lang/Throwable;)V\n\tpublic fun wtf (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V\n}\n\n"
  },
  {
    "path": "timber/build.gradle",
    "content": "import tapmoc.Severity\n\napply plugin: 'org.jetbrains.kotlin.multiplatform'\napply plugin: 'com.android.kotlin.multiplatform.library'\napply plugin: 'com.vanniktech.maven.publish'\napply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.\napply plugin: 'com.gradleup.tapmoc'\n\nkotlin {\n  androidLibrary {\n    namespace = 'timber.log'\n    compileSdk = libs.versions.compileSdk.get().toInteger()\n    minSdk = libs.versions.minSdk.get().toInteger()\n\n    optimization {\n      // \"it\" --> https://issuetracker.google.com/issues/445115242\n      it.consumerKeepRules.file('consumer-keep-rules.pro')\n    }\n\n    lint {\n      it.textReport = true\n    }\n  }\n\n  sourceSets {\n    commonMain {\n      dependencies {\n        implementation libs.annotations\n      }\n    }\n    commonTest {\n      dependencies {\n        implementation libs.annotations\n        implementation libs.junit\n        implementation libs.assertk\n        implementation libs.robolectric\n      }\n    }\n  }\n\n  abiValidation {\n    enabled = true\n  }\n}\n\ntapmoc {\n  java(8)\n  kotlin('2.0.0')\n  checkDependencies(Severity.ERROR)\n}\n\ntasks.named('check') { check ->\n  check.dependsOn(\n      // TODO: https://youtrack.jetbrains.com/issue/KT-78525\n      tasks.named('checkLegacyAbi'),\n  )\n}\n\ndependencies {\n  lintPublish project(':timber-lint')\n}\n"
  },
  {
    "path": "timber/consumer-keep-rules.pro",
    "content": "-dontwarn org.jetbrains.annotations.**\n"
  },
  {
    "path": "timber/gradle.properties",
    "content": "POM_ARTIFACT_ID=timber\nPOM_NAME=Timber\n"
  },
  {
    "path": "timber/src/androidMain/kotlin/timber/log/Timber.kt",
    "content": "package timber.log\n\nimport android.os.Build\nimport android.util.Log\nimport java.io.PrintWriter\nimport java.io.StringWriter\nimport java.util.ArrayList\nimport java.util.Collections\nimport java.util.Collections.unmodifiableList\nimport java.util.regex.Pattern\nimport org.jetbrains.annotations.NonNls\n\n/** Logging for lazy people. */\nactual class Timber actual private constructor() {\n  init {\n    throw AssertionError()\n  }\n\n  /** A facade for handling logging calls. Install instances via [`Timber.plant()`][.plant]. */\n  actual abstract class Tree {\n    @get:JvmSynthetic // Hide from public API.\n    internal val explicitTag = ThreadLocal<String>()\n\n    @get:JvmSynthetic // Hide from public API.\n    internal open val tag: String?\n      get() {\n        val tag = explicitTag.get()\n        if (tag != null) {\n          explicitTag.remove()\n        }\n        return tag\n      }\n\n    /** Log a verbose message with optional format args. */\n    actual open fun v(message: String?, vararg args: Any?) {\n      prepareLog(Log.VERBOSE, null, message, *args)\n    }\n\n    /** Log a verbose exception and a message with optional format args. */\n    actual open fun v(t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(Log.VERBOSE, t, message, *args)\n    }\n\n    /** Log a verbose exception. */\n    actual open fun v(t: Throwable?) {\n      prepareLog(Log.VERBOSE, t, null)\n    }\n\n    /** Log a debug message with optional format args. */\n    actual open fun d(message: String?, vararg args: Any?) {\n      prepareLog(Log.DEBUG, null, message, *args)\n    }\n\n    /** Log a debug exception and a message with optional format args. */\n    actual open fun d(t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(Log.DEBUG, t, message, *args)\n    }\n\n    /** Log a debug exception. */\n    actual open fun d(t: Throwable?) {\n      prepareLog(Log.DEBUG, t, null)\n    }\n\n    /** Log an info message with optional format args. */\n    actual open fun i(message: String?, vararg args: Any?) {\n      prepareLog(Log.INFO, null, message, *args)\n    }\n\n    /** Log an info exception and a message with optional format args. */\n    actual open fun i(t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(Log.INFO, t, message, *args)\n    }\n\n    /** Log an info exception. */\n    actual open fun i(t: Throwable?) {\n      prepareLog(Log.INFO, t, null)\n    }\n\n    /** Log a warning message with optional format args. */\n    actual open fun w(message: String?, vararg args: Any?) {\n      prepareLog(Log.WARN, null, message, *args)\n    }\n\n    /** Log a warning exception and a message with optional format args. */\n    actual open fun w(t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(Log.WARN, t, message, *args)\n    }\n\n    /** Log a warning exception. */\n    actual open fun w(t: Throwable?) {\n      prepareLog(Log.WARN, t, null)\n    }\n\n    /** Log an error message with optional format args. */\n    actual open fun e(message: String?, vararg args: Any?) {\n      prepareLog(Log.ERROR, null, message, *args)\n    }\n\n    /** Log an error exception and a message with optional format args. */\n    actual open fun e(t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(Log.ERROR, t, message, *args)\n    }\n\n    /** Log an error exception. */\n    actual open fun e(t: Throwable?) {\n      prepareLog(Log.ERROR, t, null)\n    }\n\n    /** Log an assert message with optional format args. */\n    actual open fun wtf(message: String?, vararg args: Any?) {\n      prepareLog(Log.ASSERT, null, message, *args)\n    }\n\n    /** Log an assert exception and a message with optional format args. */\n    actual open fun wtf(t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(Log.ASSERT, t, message, *args)\n    }\n\n    /** Log an assert exception. */\n    actual open fun wtf(t: Throwable?) {\n      prepareLog(Log.ASSERT, t, null)\n    }\n\n    /** Log at `priority` a message with optional format args. */\n    actual open fun log(priority: Int, message: String?, vararg args: Any?) {\n      prepareLog(priority, null, message, *args)\n    }\n\n    /** Log at `priority` an exception and a message with optional format args. */\n    actual open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?) {\n      prepareLog(priority, t, message, *args)\n    }\n\n    /** Log at `priority` an exception. */\n    actual open fun log(priority: Int, t: Throwable?) {\n      prepareLog(priority, t, null)\n    }\n\n    /** Return whether a message at `priority` should be logged. */\n    @Deprecated(\"Use isLoggable(String, int)\", ReplaceWith(\"this.isLoggable(null, priority)\"))\n    protected open fun isLoggable(priority: Int): Boolean = true\n\n    /** Return whether a message at `priority` or `tag` should be logged. */\n    actual protected open fun isLoggable(tag: String?, priority: Int): Boolean =\n      isLoggable(priority)\n\n    private fun prepareLog(priority: Int, t: Throwable?, message: String?, vararg args: Any?) {\n      // Consume tag even when message is not loggable so that next message is correctly tagged.\n      val tag = tag\n      if (!isLoggable(tag, priority)) {\n        return\n      }\n\n      var message = message\n      if (message.isNullOrEmpty()) {\n        if (t == null) {\n          return // Swallow message if it's null and there's no throwable.\n        }\n        message = getStackTraceString(t)\n      } else {\n        if (args.isNotEmpty()) {\n          message = formatMessage(message, args)\n        }\n        if (t != null) {\n          message += \"\\n\" + getStackTraceString(t)\n        }\n      }\n\n      log(priority, tag, message, t)\n    }\n\n    /** Formats a log message with optional arguments. */\n    actual protected open fun formatMessage(message: String, args: Array<out Any?>): String =\n      message.format(*args)\n\n    private fun getStackTraceString(t: Throwable): String {\n      // Don't replace this with Log.getStackTraceString() - it hides\n      // UnknownHostException, which is not what we want.\n      val sw = StringWriter(256)\n      val pw = PrintWriter(sw, false)\n      t.printStackTrace(pw)\n      pw.flush()\n      return sw.toString()\n    }\n\n    /**\n     * Write a log message to its destination. Called for all level-specific methods by default.\n     *\n     * @param priority Log level. See [Log] for constants.\n     * @param tag Explicit or inferred tag. May be `null`.\n     * @param message Formatted log message.\n     * @param t Accompanying exceptions. May be `null`.\n     */\n    actual protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?)\n  }\n\n  /** A [Tree] for debug builds. Automatically infers the tag from the calling class. */\n  open class DebugTree : Tree() {\n    private val fqcnIgnore =\n      listOf(\n        Timber::class.java.name,\n        Forest::class.java.name,\n        Tree::class.java.name,\n        DebugTree::class.java.name,\n      )\n\n    override val tag: String?\n      get() =\n        super.tag\n          ?: Throwable()\n            .stackTrace\n            .first { it.className !in fqcnIgnore }\n            .let(::createStackElementTag)\n\n    /**\n     * Extract the tag which should be used for the message from the `element`. By default this will\n     * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`).\n     *\n     * Note: This will not be called if a [manual tag][.tag] was specified.\n     */\n    protected open fun createStackElementTag(element: StackTraceElement): String? {\n      var tag = element.className.substringAfterLast('.')\n      val m = ANONYMOUS_CLASS.matcher(tag)\n      if (m.find()) {\n        tag = m.replaceAll(\"\")\n      }\n      // Tag length limit was removed in API 26.\n      return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {\n        tag\n      } else {\n        tag.substring(0, MAX_TAG_LENGTH)\n      }\n    }\n\n    /**\n     * Break up `message` into maximum-length chunks (if needed) and send to either\n     * [Log.println()][Log.println] or [Log.wtf()][Log.wtf] for logging.\n     *\n     * {@inheritDoc}\n     */\n    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {\n      if (message.length < MAX_LOG_LENGTH) {\n        if (priority == Log.ASSERT) {\n          Log.wtf(tag, message)\n        } else {\n          Log.println(priority, tag, message)\n        }\n        return\n      }\n\n      // Split by line, then ensure each line can fit into Log's maximum length.\n      var i = 0\n      val length = message.length\n      while (i < length) {\n        var newline = message.indexOf('\\n', i)\n        newline = if (newline != -1) newline else length\n        do {\n          val end = Math.min(newline, i + MAX_LOG_LENGTH)\n          val part = message.substring(i, end)\n          if (priority == Log.ASSERT) {\n            Log.wtf(tag, part)\n          } else {\n            Log.println(priority, tag, part)\n          }\n          i = end\n        } while (i < newline)\n        i++\n      }\n    }\n\n    companion object {\n      private const val MAX_LOG_LENGTH = 4000\n      private const val MAX_TAG_LENGTH = 23\n      private val ANONYMOUS_CLASS = Pattern.compile(\"(\\\\$\\\\d+)+$\")\n    }\n  }\n\n  actual companion object Forest : Tree() {\n    /** Log a verbose message with optional format args. */\n    @JvmStatic\n    override fun v(@NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.v(message, *args) }\n    }\n\n    /** Log a verbose exception and a message with optional format args. */\n    @JvmStatic\n    override fun v(t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.v(t, message, *args) }\n    }\n\n    /** Log a verbose exception. */\n    @JvmStatic\n    override fun v(t: Throwable?) {\n      treeArray.forEach { it.v(t) }\n    }\n\n    /** Log a debug message with optional format args. */\n    @JvmStatic\n    override fun d(@NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.d(message, *args) }\n    }\n\n    /** Log a debug exception and a message with optional format args. */\n    @JvmStatic\n    override fun d(t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.d(t, message, *args) }\n    }\n\n    /** Log a debug exception. */\n    @JvmStatic\n    override fun d(t: Throwable?) {\n      treeArray.forEach { it.d(t) }\n    }\n\n    /** Log an info message with optional format args. */\n    @JvmStatic\n    override fun i(@NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.i(message, *args) }\n    }\n\n    /** Log an info exception and a message with optional format args. */\n    @JvmStatic\n    override fun i(t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.i(t, message, *args) }\n    }\n\n    /** Log an info exception. */\n    @JvmStatic\n    override fun i(t: Throwable?) {\n      treeArray.forEach { it.i(t) }\n    }\n\n    /** Log a warning message with optional format args. */\n    @JvmStatic\n    override fun w(@NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.w(message, *args) }\n    }\n\n    /** Log a warning exception and a message with optional format args. */\n    @JvmStatic\n    override fun w(t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.w(t, message, *args) }\n    }\n\n    /** Log a warning exception. */\n    @JvmStatic\n    override fun w(t: Throwable?) {\n      treeArray.forEach { it.w(t) }\n    }\n\n    /** Log an error message with optional format args. */\n    @JvmStatic\n    override fun e(@NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.e(message, *args) }\n    }\n\n    /** Log an error exception and a message with optional format args. */\n    @JvmStatic\n    override fun e(t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.e(t, message, *args) }\n    }\n\n    /** Log an error exception. */\n    @JvmStatic\n    override fun e(t: Throwable?) {\n      treeArray.forEach { it.e(t) }\n    }\n\n    /** Log an assert message with optional format args. */\n    @JvmStatic\n    override fun wtf(@NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.wtf(message, *args) }\n    }\n\n    /** Log an assert exception and a message with optional format args. */\n    @JvmStatic\n    override fun wtf(t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.wtf(t, message, *args) }\n    }\n\n    /** Log an assert exception. */\n    @JvmStatic\n    override fun wtf(t: Throwable?) {\n      treeArray.forEach { it.wtf(t) }\n    }\n\n    /** Log at `priority` a message with optional format args. */\n    @JvmStatic\n    override fun log(priority: Int, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.log(priority, message, *args) }\n    }\n\n    /** Log at `priority` an exception and a message with optional format args. */\n    @JvmStatic\n    override fun log(priority: Int, t: Throwable?, @NonNls message: String?, vararg args: Any?) {\n      treeArray.forEach { it.log(priority, t, message, *args) }\n    }\n\n    /** Log at `priority` an exception. */\n    @JvmStatic\n    override fun log(priority: Int, t: Throwable?) {\n      treeArray.forEach { it.log(priority, t) }\n    }\n\n    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {\n      throw AssertionError() // Missing override for log method.\n    }\n\n    /**\n     * A view into Timber's planted trees as a tree itself. This can be used for injecting a logger\n     * instance rather than using static methods or to facilitate testing.\n     */\n    @Suppress(\n      \"NOTHING_TO_INLINE\", // Kotlin users should reference `Tree.Forest` directly.\n      \"NON_FINAL_MEMBER_IN_OBJECT\", // For japicmp check.\n    )\n    @JvmStatic\n    open inline fun asTree(): Tree = this\n\n    /** Set a one-time tag for use on the next logging call. */\n    @JvmStatic\n    actual fun tag(tag: String): Tree {\n      for (tree in treeArray) {\n        tree.explicitTag.set(tag)\n      }\n      return this\n    }\n\n    /** Add a new logging tree. */\n    @JvmStatic\n    actual fun plant(tree: Tree) {\n      require(tree !== this) { \"Cannot plant Timber into itself.\" }\n      synchronized(trees) {\n        trees.add(tree)\n        treeArray = trees.toTypedArray()\n      }\n    }\n\n    /** Adds new logging trees. */\n    @JvmStatic\n    actual fun plant(vararg trees: Tree) {\n      for (tree in trees) {\n        requireNotNull(tree) { \"trees contained null\" }\n        require(tree !== this) { \"Cannot plant Timber into itself.\" }\n      }\n      synchronized(this.trees) {\n        Collections.addAll(this.trees, *trees)\n        treeArray = this.trees.toTypedArray()\n      }\n    }\n\n    /** Remove a planted tree. */\n    @JvmStatic\n    actual fun uproot(tree: Tree) {\n      synchronized(trees) {\n        require(trees.remove(tree)) { \"Cannot uproot tree which is not planted: $tree\" }\n        treeArray = trees.toTypedArray()\n      }\n    }\n\n    /** Remove all planted trees. */\n    @JvmStatic\n    actual fun uprootAll() {\n      synchronized(trees) {\n        trees.clear()\n        treeArray = emptyArray()\n      }\n    }\n\n    /** Return a copy of all planted [trees][Tree]. */\n    @JvmStatic\n    actual fun forest(): List<Tree> {\n      synchronized(trees) {\n        return unmodifiableList(trees.toList())\n      }\n    }\n\n    @get:[JvmStatic JvmName(\"treeCount\")]\n    actual val treeCount\n      get() = treeArray.size\n\n    // Both fields guarded by 'trees'.\n    private val trees = ArrayList<Tree>()\n    @Volatile private var treeArray = emptyArray<Tree>()\n  }\n}\n"
  },
  {
    "path": "timber/src/commonMain/kotlin/timber/log/Timber.kt",
    "content": "package timber.log\n\n/** Logging for lazy people. */\nexpect class Timber private constructor() {\n\n  /** A facade for handling logging calls. Install instances via [`Timber.plant()`][.plant]. */\n  abstract class Tree {\n\n    /** Log a verbose message with optional format args. */\n    open fun v(message: String?, vararg args: Any?)\n\n    /** Log a verbose exception and a message with optional format args. */\n    open fun v(t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log a verbose exception. */\n    open fun v(t: Throwable?)\n\n    /** Log a debug message with optional format args. */\n    open fun d(message: String?, vararg args: Any?)\n\n    /** Log a debug exception and a message with optional format args. */\n    open fun d(t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log a debug exception. */\n    open fun d(t: Throwable?)\n\n    /** Log an info message with optional format args. */\n    open fun i(message: String?, vararg args: Any?)\n\n    /** Log an info exception and a message with optional format args. */\n    open fun i(t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log an info exception. */\n    open fun i(t: Throwable?)\n\n    /** Log a warning message with optional format args. */\n    open fun w(message: String?, vararg args: Any?)\n\n    /** Log a warning exception and a message with optional format args. */\n    open fun w(t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log a warning exception. */\n    open fun w(t: Throwable?)\n\n    /** Log an error message with optional format args. */\n    open fun e(message: String?, vararg args: Any?)\n\n    /** Log an error exception and a message with optional format args. */\n    open fun e(t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log an error exception. */\n    open fun e(t: Throwable?)\n\n    /** Log an assert message with optional format args. */\n    open fun wtf(message: String?, vararg args: Any?)\n\n    /** Log an assert exception and a message with optional format args. */\n    open fun wtf(t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log an assert exception. */\n    open fun wtf(t: Throwable?)\n\n    /** Log at `priority` a message with optional format args. */\n    open fun log(priority: Int, message: String?, vararg args: Any?)\n\n    /** Log at `priority` an exception and a message with optional format args. */\n    open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?)\n\n    /** Log at `priority` an exception. */\n    open fun log(priority: Int, t: Throwable?)\n\n    /** Return whether a message at `priority` or `tag` should be logged. */\n    protected open fun isLoggable(tag: String?, priority: Int): Boolean\n\n    /** Formats a log message with optional arguments. */\n    protected open fun formatMessage(message: String, args: Array<out Any?>): String\n\n    /**\n     * Write a log message to its destination. Called for all level-specific methods by default.\n     *\n     * @param priority Log level. See [Log] for constants.\n     * @param tag Explicit or inferred tag. May be `null`.\n     * @param message Formatted log message.\n     * @param t Accompanying exceptions. May be `null`.\n     */\n    protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?)\n  }\n\n  companion object Forest : Tree {\n\n    /** Set a one-time tag for use on the next logging call. */\n    fun tag(tag: String): Tree\n\n    /** Add a new logging tree. */\n    fun plant(tree: Tree)\n\n    /** Adds new logging trees. */\n    fun plant(vararg trees: Tree)\n\n    /** Remove a planted tree. */\n    fun uproot(tree: Tree)\n\n    /** Remove all planted trees. */\n    fun uprootAll()\n\n    /** Return a copy of all planted [trees][Tree]. */\n    fun forest(): List<Tree>\n\n    val treeCount: Int\n  }\n}\n"
  },
  {
    "path": "timber/src/test/java/timber/log/TimberJavaTest.java",
    "content": "package timber.log;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.fail;\n\npublic class TimberJavaTest {\n  @SuppressWarnings(\"ConstantConditions\")\n  @Test public void nullTree() {\n    try {\n      Timber.plant((Timber.Tree) null);\n      fail();\n    } catch (NullPointerException ignored) {\n    }\n  }\n\n  @SuppressWarnings(\"ConstantConditions\")\n  @Test public void nullTreeArray() {\n    try {\n      Timber.plant((Timber.Tree[]) null);\n      fail();\n    } catch (NullPointerException ignored) {\n    }\n    try {\n      Timber.plant(new Timber.Tree[] { null });\n      fail();\n    } catch (IllegalArgumentException ignored) {\n    }\n  }\n}\n"
  },
  {
    "path": "timber/src/test/java/timber/log/TimberTest.kt",
    "content": "package timber.log\n\nimport android.os.Build\nimport android.util.Log\nimport assertk.assertFailure\nimport assertk.assertThat\nimport assertk.assertions.contains\nimport assertk.assertions.containsExactly\nimport assertk.assertions.hasMessage\nimport assertk.assertions.hasSize\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isInstanceOf\nimport assertk.assertions.isNotNull\nimport assertk.assertions.isNull\nimport assertk.assertions.message\nimport assertk.assertions.startsWith\nimport java.net.ConnectException\nimport java.net.UnknownHostException\nimport java.util.ArrayList\nimport java.util.concurrent.CountDownLatch\nimport org.junit.After\nimport org.junit.Assert.assertTrue\nimport org.junit.Assert.fail\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\nimport org.robolectric.annotation.Config\nimport org.robolectric.shadows.ShadowLog\nimport org.robolectric.shadows.ShadowLog.LogItem\n\n@RunWith(RobolectricTestRunner::class)\n@Config(manifest = Config.NONE)\nclass TimberTest {\n  @Before\n  @After\n  fun setUpAndTearDown() {\n    Timber.uprootAll()\n  }\n\n  // NOTE: This class references the line number. Keep it at the top so it does not change.\n  @Test\n  fun debugTreeCanAlterCreatedTag() {\n    Timber.plant(\n      object : Timber.DebugTree() {\n        override fun createStackElementTag(element: StackTraceElement): String? {\n          return super.createStackElementTag(element) + ':'.toString() + element.lineNumber\n        }\n      }\n    )\n\n    Timber.d(\"Test\")\n\n    assertLog().hasDebugMessage(\"TimberTest:48\", \"Test\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun recursion() {\n    val timber = Timber.asTree()\n\n    assertFailure { Timber.plant(timber) }\n      .isInstanceOf<IllegalArgumentException>()\n      .hasMessage(\"Cannot plant Timber into itself.\")\n\n    assertFailure {\n        @Suppress(\"RemoveRedundantSpreadOperator\") // Explicitly calling vararg overload.\n        Timber.plant(*arrayOf(timber))\n      }\n      .isInstanceOf<IllegalArgumentException>()\n      .hasMessage(\"Cannot plant Timber into itself.\")\n  }\n\n  @Test\n  fun treeCount() {\n    // inserts trees and checks if the amount of returned trees matches.\n    assertThat(Timber.treeCount).isEqualTo(0)\n    for (i in 1 until 50) {\n      Timber.plant(Timber.DebugTree())\n      assertThat(Timber.treeCount).isEqualTo(i)\n    }\n    Timber.uprootAll()\n    assertThat(Timber.treeCount).isEqualTo(0)\n  }\n\n  @Test\n  fun forestReturnsAllPlanted() {\n    val tree1 = Timber.DebugTree()\n    val tree2 = Timber.DebugTree()\n    Timber.plant(tree1)\n    Timber.plant(tree2)\n\n    assertThat(Timber.forest()).containsExactly(tree1, tree2)\n  }\n\n  @Test\n  fun forestReturnsAllTreesPlanted() {\n    val tree1 = Timber.DebugTree()\n    val tree2 = Timber.DebugTree()\n    Timber.plant(tree1, tree2)\n\n    assertThat(Timber.forest()).containsExactly(tree1, tree2)\n  }\n\n  @Test\n  fun uprootThrowsIfMissing() {\n    assertFailure { Timber.uproot(Timber.DebugTree()) }\n      .isInstanceOf<IllegalArgumentException>()\n      .message()\n      .isNotNull()\n      .startsWith(\"Cannot uproot tree which is not planted: \")\n  }\n\n  @Test\n  fun uprootRemovesTree() {\n    val tree1 = Timber.DebugTree()\n    val tree2 = Timber.DebugTree()\n    Timber.plant(tree1)\n    Timber.plant(tree2)\n    Timber.d(\"First\")\n    Timber.uproot(tree1)\n    Timber.d(\"Second\")\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\", \"First\")\n      .hasDebugMessage(\"TimberTest\", \"First\")\n      .hasDebugMessage(\"TimberTest\", \"Second\")\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun uprootAllRemovesAll() {\n    val tree1 = Timber.DebugTree()\n    val tree2 = Timber.DebugTree()\n    Timber.plant(tree1)\n    Timber.plant(tree2)\n    Timber.d(\"First\")\n    Timber.uprootAll()\n    Timber.d(\"Second\")\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\", \"First\")\n      .hasDebugMessage(\"TimberTest\", \"First\")\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun noArgsDoesNotFormat() {\n    Timber.plant(Timber.DebugTree())\n    Timber.d(\"te%st\")\n\n    assertLog().hasDebugMessage(\"TimberTest\", \"te%st\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun debugTreeTagGeneration() {\n    Timber.plant(Timber.DebugTree())\n    Timber.d(\"Hello, world!\")\n\n    assertLog().hasDebugMessage(\"TimberTest\", \"Hello, world!\").hasNoMoreMessages()\n  }\n\n  internal inner class ThisIsAReallyLongClassName {\n    fun run() {\n      Timber.d(\"Hello, world!\")\n    }\n  }\n\n  @Config(sdk = [25])\n  @Test\n  fun debugTreeTagTruncation() {\n    Timber.plant(Timber.DebugTree())\n\n    ThisIsAReallyLongClassName().run()\n\n    assertLog().hasDebugMessage(\"TimberTest\\$ThisIsAReall\", \"Hello, world!\").hasNoMoreMessages()\n  }\n\n  @Config(sdk = [26])\n  @Test\n  fun debugTreeTagNoTruncation() {\n    Timber.plant(Timber.DebugTree())\n\n    ThisIsAReallyLongClassName().run()\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\\$ThisIsAReallyLongClassName\", \"Hello, world!\")\n      .hasNoMoreMessages()\n  }\n\n  @Suppress(\"ObjectLiteralToLambda\") // Lambdas != anonymous classes.\n  @Test\n  fun debugTreeTagGenerationStripsAnonymousClassMarker() {\n    Timber.plant(Timber.DebugTree())\n    object : Runnable {\n        override fun run() {\n          Timber.d(\"Hello, world!\")\n\n          object : Runnable {\n              override fun run() {\n                Timber.d(\"Hello, world!\")\n              }\n            }\n            .run()\n        }\n      }\n      .run()\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\\$debugTreeTag\", \"Hello, world!\")\n      .hasDebugMessage(\"TimberTest\\$debugTreeTag\", \"Hello, world!\")\n      .hasNoMoreMessages()\n  }\n\n  @Suppress(\"ObjectLiteralToLambda\") // Lambdas != anonymous classes.\n  @Test\n  fun debugTreeTagGenerationStripsAnonymousClassMarkerWithInnerSAMLambda() {\n    Timber.plant(Timber.DebugTree())\n    object : Runnable {\n        override fun run() {\n          Timber.d(\"Hello, world!\")\n\n          Runnable { Timber.d(\"Hello, world!\") }.run()\n        }\n      }\n      .run()\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\\$debugTreeTag\", \"Hello, world!\")\n      .hasDebugMessage(\"TimberTest\\$debugTreeTag\", \"Hello, world!\")\n      .hasNoMoreMessages()\n  }\n\n  @Suppress(\"ObjectLiteralToLambda\") // Lambdas != anonymous classes.\n  @Test\n  fun debugTreeTagGenerationStripsAnonymousClassMarkerWithOuterSAMLambda() {\n    Timber.plant(Timber.DebugTree())\n\n    Runnable {\n        Timber.d(\"Hello, world!\")\n\n        object : Runnable {\n            override fun run() {\n              Timber.d(\"Hello, world!\")\n            }\n          }\n          .run()\n      }\n      .run()\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\", \"Hello, world!\")\n      .hasDebugMessage(\"TimberTest\\$debugTreeTag\", \"Hello, world!\")\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun debugTreeTagGenerationStripsAnonymousLambdaClassMarker() {\n    Timber.plant(Timber.DebugTree())\n\n    val outer = {\n      Timber.d(\"Hello, world!\")\n\n      val inner = { Timber.d(\"Hello, world!\") }\n\n      inner()\n    }\n\n    outer()\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\", \"Hello, world!\")\n      .hasDebugMessage(\"TimberTest\", \"Hello, world!\")\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun debugTreeTagGenerationForSAMLambdasUsesClassName() {\n    Timber.plant(Timber.DebugTree())\n\n    Runnable {\n        Timber.d(\"Hello, world!\")\n\n        Runnable { Timber.d(\"Hello, world!\") }.run()\n      }\n      .run()\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\", \"Hello, world!\")\n      .hasDebugMessage(\"TimberTest\", \"Hello, world!\")\n      .hasNoMoreMessages()\n  }\n\n  private class ClassNameThatIsReallyReallyReallyLong {\n    init {\n      Timber.i(\"Hello, world!\")\n    }\n  }\n\n  @Test\n  fun debugTreeGeneratedTagIsLoggable() {\n    Timber.plant(\n      object : Timber.DebugTree() {\n        private val MAX_TAG_LENGTH = 23\n\n        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {\n          try {\n            assertTrue(Log.isLoggable(tag, priority))\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {\n              assertTrue(tag!!.length <= MAX_TAG_LENGTH)\n            }\n          } catch (e: IllegalArgumentException) {\n            fail(e.message)\n          }\n\n          super.log(priority, tag, message, t)\n        }\n      }\n    )\n    ClassNameThatIsReallyReallyReallyLong()\n    assertLog().hasInfoMessage(\"TimberTest\\$ClassNameTha\", \"Hello, world!\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun debugTreeCustomTag() {\n    Timber.plant(Timber.DebugTree())\n    Timber.tag(\"Custom\").d(\"Hello, world!\")\n\n    assertLog().hasDebugMessage(\"Custom\", \"Hello, world!\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun messageWithException() {\n    Timber.plant(Timber.DebugTree())\n    val datThrowable = truncatedThrowable(NullPointerException::class.java)\n    Timber.e(datThrowable, \"OMFG!\")\n\n    assertExceptionLogged(Log.ERROR, \"OMFG!\", \"java.lang.NullPointerException\")\n  }\n\n  @Test\n  fun exceptionOnly() {\n    Timber.plant(Timber.DebugTree())\n\n    Timber.v(truncatedThrowable(IllegalArgumentException::class.java))\n    assertExceptionLogged(Log.VERBOSE, null, \"java.lang.IllegalArgumentException\", \"TimberTest\", 0)\n\n    Timber.i(truncatedThrowable(NullPointerException::class.java))\n    assertExceptionLogged(Log.INFO, null, \"java.lang.NullPointerException\", \"TimberTest\", 1)\n\n    Timber.d(truncatedThrowable(UnsupportedOperationException::class.java))\n    assertExceptionLogged(\n      Log.DEBUG,\n      null,\n      \"java.lang.UnsupportedOperationException\",\n      \"TimberTest\",\n      2,\n    )\n\n    Timber.w(truncatedThrowable(UnknownHostException::class.java))\n    assertExceptionLogged(Log.WARN, null, \"java.net.UnknownHostException\", \"TimberTest\", 3)\n\n    Timber.e(truncatedThrowable(ConnectException::class.java))\n    assertExceptionLogged(Log.ERROR, null, \"java.net.ConnectException\", \"TimberTest\", 4)\n\n    Timber.wtf(truncatedThrowable(AssertionError::class.java))\n    assertExceptionLogged(Log.ASSERT, null, \"java.lang.AssertionError\", \"TimberTest\", 5)\n  }\n\n  @Test\n  fun exceptionOnlyCustomTag() {\n    Timber.plant(Timber.DebugTree())\n\n    Timber.tag(\"Custom\").v(truncatedThrowable(IllegalArgumentException::class.java))\n    assertExceptionLogged(Log.VERBOSE, null, \"java.lang.IllegalArgumentException\", \"Custom\", 0)\n\n    Timber.tag(\"Custom\").i(truncatedThrowable(NullPointerException::class.java))\n    assertExceptionLogged(Log.INFO, null, \"java.lang.NullPointerException\", \"Custom\", 1)\n\n    Timber.tag(\"Custom\").d(truncatedThrowable(UnsupportedOperationException::class.java))\n    assertExceptionLogged(Log.DEBUG, null, \"java.lang.UnsupportedOperationException\", \"Custom\", 2)\n\n    Timber.tag(\"Custom\").w(truncatedThrowable(UnknownHostException::class.java))\n    assertExceptionLogged(Log.WARN, null, \"java.net.UnknownHostException\", \"Custom\", 3)\n\n    Timber.tag(\"Custom\").e(truncatedThrowable(ConnectException::class.java))\n    assertExceptionLogged(Log.ERROR, null, \"java.net.ConnectException\", \"Custom\", 4)\n\n    Timber.tag(\"Custom\").wtf(truncatedThrowable(AssertionError::class.java))\n    assertExceptionLogged(Log.ASSERT, null, \"java.lang.AssertionError\", \"Custom\", 5)\n  }\n\n  @Test\n  fun exceptionFromSpawnedThread() {\n    Timber.plant(Timber.DebugTree())\n    val datThrowable = truncatedThrowable(NullPointerException::class.java)\n    val latch = CountDownLatch(1)\n    object : Thread() {\n        override fun run() {\n          Timber.e(datThrowable, \"OMFG!\")\n          latch.countDown()\n        }\n      }\n      .start()\n    latch.await()\n    assertExceptionLogged(\n      Log.ERROR,\n      \"OMFG!\",\n      \"java.lang.NullPointerException\",\n      \"TimberTest\\$exceptionFro\",\n    )\n  }\n\n  @Test\n  fun nullMessageWithThrowable() {\n    Timber.plant(Timber.DebugTree())\n    val datThrowable = truncatedThrowable(NullPointerException::class.java)\n    Timber.e(datThrowable, null)\n\n    assertExceptionLogged(Log.ERROR, \"\", \"java.lang.NullPointerException\")\n  }\n\n  @Test\n  fun chunkAcrossNewlinesAndLimit() {\n    Timber.plant(Timber.DebugTree())\n    Timber.d(\n      'a'.repeat(3000) + '\\n'.toString() + 'b'.repeat(6000) + '\\n'.toString() + 'c'.repeat(3000)\n    )\n\n    assertLog()\n      .hasDebugMessage(\"TimberTest\", 'a'.repeat(3000))\n      .hasDebugMessage(\"TimberTest\", 'b'.repeat(4000))\n      .hasDebugMessage(\"TimberTest\", 'b'.repeat(2000))\n      .hasDebugMessage(\"TimberTest\", 'c'.repeat(3000))\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun nullMessageWithoutThrowable() {\n    Timber.plant(Timber.DebugTree())\n    Timber.d(null as String?)\n\n    assertLog().hasNoMoreMessages()\n  }\n\n  @Test\n  fun logMessageCallback() {\n    val logs = ArrayList<String>()\n    Timber.plant(\n      object : Timber.DebugTree() {\n        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {\n          logs.add(\"$priority $tag $message\")\n        }\n      }\n    )\n\n    Timber.v(\"Verbose\")\n    Timber.tag(\"Custom\").v(\"Verbose\")\n    Timber.d(\"Debug\")\n    Timber.tag(\"Custom\").d(\"Debug\")\n    Timber.i(\"Info\")\n    Timber.tag(\"Custom\").i(\"Info\")\n    Timber.w(\"Warn\")\n    Timber.tag(\"Custom\").w(\"Warn\")\n    Timber.e(\"Error\")\n    Timber.tag(\"Custom\").e(\"Error\")\n    Timber.wtf(\"Assert\")\n    Timber.tag(\"Custom\").wtf(\"Assert\")\n\n    assertThat(logs)\n      .containsExactly( //\n        \"2 TimberTest Verbose\", //\n        \"2 Custom Verbose\", //\n        \"3 TimberTest Debug\", //\n        \"3 Custom Debug\", //\n        \"4 TimberTest Info\", //\n        \"4 Custom Info\", //\n        \"5 TimberTest Warn\", //\n        \"5 Custom Warn\", //\n        \"6 TimberTest Error\", //\n        \"6 Custom Error\", //\n        \"7 TimberTest Assert\", //\n        \"7 Custom Assert\", //\n      )\n  }\n\n  @Test\n  fun logAtSpecifiedPriority() {\n    Timber.plant(Timber.DebugTree())\n\n    Timber.log(Log.VERBOSE, \"Hello, World!\")\n    Timber.log(Log.DEBUG, \"Hello, World!\")\n    Timber.log(Log.INFO, \"Hello, World!\")\n    Timber.log(Log.WARN, \"Hello, World!\")\n    Timber.log(Log.ERROR, \"Hello, World!\")\n    Timber.log(Log.ASSERT, \"Hello, World!\")\n\n    assertLog()\n      .hasVerboseMessage(\"TimberTest\", \"Hello, World!\")\n      .hasDebugMessage(\"TimberTest\", \"Hello, World!\")\n      .hasInfoMessage(\"TimberTest\", \"Hello, World!\")\n      .hasWarnMessage(\"TimberTest\", \"Hello, World!\")\n      .hasErrorMessage(\"TimberTest\", \"Hello, World!\")\n      .hasAssertMessage(\"TimberTest\", \"Hello, World!\")\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun formatting() {\n    Timber.plant(Timber.DebugTree())\n    Timber.v(\"Hello, %s!\", \"World\")\n    Timber.d(\"Hello, %s!\", \"World\")\n    Timber.i(\"Hello, %s!\", \"World\")\n    Timber.w(\"Hello, %s!\", \"World\")\n    Timber.e(\"Hello, %s!\", \"World\")\n    Timber.wtf(\"Hello, %s!\", \"World\")\n\n    assertLog()\n      .hasVerboseMessage(\"TimberTest\", \"Hello, World!\")\n      .hasDebugMessage(\"TimberTest\", \"Hello, World!\")\n      .hasInfoMessage(\"TimberTest\", \"Hello, World!\")\n      .hasWarnMessage(\"TimberTest\", \"Hello, World!\")\n      .hasErrorMessage(\"TimberTest\", \"Hello, World!\")\n      .hasAssertMessage(\"TimberTest\", \"Hello, World!\")\n      .hasNoMoreMessages()\n  }\n\n  @Test\n  fun isLoggableControlsLogging() {\n    Timber.plant(\n      object : Timber.DebugTree() {\n        @Suppress(\"OverridingDeprecatedMember\") // Explicitly testing deprecated variant.\n        override fun isLoggable(priority: Int): Boolean {\n          return priority == Log.INFO\n        }\n      }\n    )\n    Timber.v(\"Hello, World!\")\n    Timber.d(\"Hello, World!\")\n    Timber.i(\"Hello, World!\")\n    Timber.w(\"Hello, World!\")\n    Timber.e(\"Hello, World!\")\n    Timber.wtf(\"Hello, World!\")\n\n    assertLog().hasInfoMessage(\"TimberTest\", \"Hello, World!\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun isLoggableTagControlsLogging() {\n    Timber.plant(\n      object : Timber.DebugTree() {\n        override fun isLoggable(tag: String?, priority: Int): Boolean {\n          return \"FILTER\" == tag\n        }\n      }\n    )\n    Timber.tag(\"FILTER\").v(\"Hello, World!\")\n    Timber.d(\"Hello, World!\")\n    Timber.i(\"Hello, World!\")\n    Timber.w(\"Hello, World!\")\n    Timber.e(\"Hello, World!\")\n    Timber.wtf(\"Hello, World!\")\n\n    assertLog().hasVerboseMessage(\"FILTER\", \"Hello, World!\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun logsUnknownHostExceptions() {\n    Timber.plant(Timber.DebugTree())\n    Timber.e(truncatedThrowable(UnknownHostException::class.java), null)\n\n    assertExceptionLogged(Log.ERROR, \"\", \"UnknownHostException\")\n  }\n\n  @Test\n  fun tagIsClearedWhenNotLoggable() {\n    Timber.plant(\n      object : Timber.DebugTree() {\n        override fun isLoggable(tag: String?, priority: Int): Boolean {\n          return priority >= Log.WARN\n        }\n      }\n    )\n    Timber.tag(\"NotLogged\").i(\"Message not logged\")\n    Timber.w(\"Message logged\")\n\n    assertLog().hasWarnMessage(\"TimberTest\", \"Message logged\").hasNoMoreMessages()\n  }\n\n  @Test\n  fun logsWithCustomFormatter() {\n    Timber.plant(\n      object : Timber.DebugTree() {\n        override fun formatMessage(message: String, vararg args: Any?): String {\n          return String.format(\"Test formatting: $message\", *args)\n        }\n      }\n    )\n    Timber.d(\"Test message logged. %d\", 100)\n\n    assertLog().hasDebugMessage(\"TimberTest\", \"Test formatting: Test message logged. 100\")\n  }\n\n  private fun <T : Throwable> truncatedThrowable(throwableClass: Class<T>): T {\n    val throwable = throwableClass.newInstance()\n    val stackTrace = throwable.stackTrace\n    val traceLength = if (stackTrace.size > 5) 5 else stackTrace.size\n    throwable.stackTrace = stackTrace.copyOf(traceLength)\n    return throwable\n  }\n\n  private fun Char.repeat(number: Int) = toString().repeat(number)\n\n  private fun assertExceptionLogged(\n    logType: Int,\n    message: String?,\n    exceptionClassname: String,\n    tag: String? = null,\n    index: Int = 0,\n  ) {\n    val logs = getLogs()\n    assertThat(logs).hasSize(index + 1)\n    val log = logs[index]\n    assertThat(log.type).isEqualTo(logType)\n    assertThat(log.tag).isEqualTo(tag ?: \"TimberTest\")\n\n    if (message != null) {\n      assertThat(log.msg).startsWith(message)\n    }\n\n    assertThat(log.msg).contains(exceptionClassname)\n    // We use a low-level primitive that Robolectric doesn't populate.\n    assertThat(log.throwable).isNull()\n  }\n\n  private fun assertLog(): LogAssert {\n    return LogAssert(getLogs())\n  }\n\n  private fun getLogs() = ShadowLog.getLogs().filter { it.tag != ROBOLECTRIC_INSTRUMENTATION_TAG }\n\n  private class LogAssert internal constructor(private val items: List<LogItem>) {\n    private var index = 0\n\n    fun hasVerboseMessage(tag: String, message: String): LogAssert {\n      return hasMessage(Log.VERBOSE, tag, message)\n    }\n\n    fun hasDebugMessage(tag: String, message: String): LogAssert {\n      return hasMessage(Log.DEBUG, tag, message)\n    }\n\n    fun hasInfoMessage(tag: String, message: String): LogAssert {\n      return hasMessage(Log.INFO, tag, message)\n    }\n\n    fun hasWarnMessage(tag: String, message: String): LogAssert {\n      return hasMessage(Log.WARN, tag, message)\n    }\n\n    fun hasErrorMessage(tag: String, message: String): LogAssert {\n      return hasMessage(Log.ERROR, tag, message)\n    }\n\n    fun hasAssertMessage(tag: String, message: String): LogAssert {\n      return hasMessage(Log.ASSERT, tag, message)\n    }\n\n    private fun hasMessage(priority: Int, tag: String, message: String): LogAssert {\n      val item = items[index++]\n      assertThat(item.type).isEqualTo(priority)\n      assertThat(item.tag).isEqualTo(tag)\n      assertThat(item.msg).isEqualTo(message)\n      return this\n    }\n\n    fun hasNoMoreMessages() {\n      assertThat(items).hasSize(index)\n    }\n  }\n\n  private companion object {\n    private const val ROBOLECTRIC_INSTRUMENTATION_TAG = \"MonitoringInstr\"\n  }\n}\n"
  },
  {
    "path": "timber-lint/build.gradle",
    "content": "apply plugin: 'org.jetbrains.kotlin.jvm'\napply plugin: 'org.jetbrains.kotlin.kapt'\napply plugin: 'com.android.lint'\napply plugin: 'com.gradleup.tapmoc'\n\ntapmoc {\n  java(17)\n}\n\nlint {\n  baseline = file(\"lint-baseline.xml\")\n}\n\ndependencies {\n  compileOnly libs.lint.api\n  compileOnly libs.lint.checks\n  compileOnly libs.auto.annotations\n  kapt libs.auto.service\n  testImplementation libs.junit\n  testImplementation libs.lint.core\n  testImplementation libs.lint.tests\n  testImplementation libs.junit\n}\n"
  },
  {
    "path": "timber-lint/gradle.properties",
    "content": "# needed so that :timber:prepareLintJarForPublish can succeed\n# Remove when the bug described in https://issuetracker.google.com/issues/161727305 is fixed\nkotlin.stdlib.default.dependency=false\n"
  },
  {
    "path": "timber-lint/lint-baseline.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<issues format=\"6\" by=\"lint 8.12.0\" type=\"baseline\" client=\"gradle\" dependencies=\"false\" name=\"AGP (8.12.0)\" variant=\"all\" version=\"8.12.0\">\n\n    <issue\n        id=\"LintImplTextFormat\"\n        message=\"&quot;format()&quot; looks like a call; surround with backtics in string to display as symbol, e.g. \\`format()\\`\"\n        errorLine1=\"      explanation = &quot;Since Timber handles String.format automatically, you may not use String#format().&quot;,\"\n        errorLine2=\"                                                                                              ~~~~~~~~\">\n        <location\n            file=\"src/main/java/timber/lint/WrongTimberUsageDetector.kt\"\n            line=\"760\"\n            column=\"95\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTextFormat\"\n        message=\"&quot;format()&quot; looks like a call; surround with backtics in string to display as symbol, e.g. \\`format()\\`\"\n        errorLine1=\"      explanation = &quot;Since Timber handles String#format() automatically, use this instead of String concatenation.&quot;,\"\n        errorLine2=\"                                                 ~~~~~~~~\">\n        <location\n            file=\"src/main/java/timber/lint/WrongTimberUsageDetector.kt\"\n            line=\"778\"\n            column=\"50\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplDollarEscapes\"\n        message=\"In unit tests, use the fullwidth dollar sign, `＄`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `＄` as a `$`.\"\n        errorLine1=\"                |     Timber.d(&quot;${&quot;$&quot;}{foo}bar&quot;)\"\n        errorLine2=\"                                ~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"684\"\n            column=\"33\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplDollarEscapes\"\n        message=\"In unit tests, use the fullwidth dollar sign, `＄`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `＄` as a `$`.\"\n        errorLine1=\"                |     Timber.d(&quot;foo${&quot;$&quot;}bar&quot;)\"\n        errorLine2=\"                                   ~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"733\"\n            column=\"36\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplDollarEscapes\"\n        message=\"In unit tests, use the fullwidth dollar sign, `＄`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `＄` as a `$`.\"\n        errorLine1=\"                |     Timber.d(&quot;${&quot;$&quot;}{foo}${&quot;$&quot;}bar&quot;)\"\n        errorLine2=\"                                ~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"784\"\n            column=\"33\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplDollarEscapes\"\n        message=\"In unit tests, use the fullwidth dollar sign, `＄`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `＄` as a `$`.\"\n        errorLine1=\"                |     Timber.d(if(true) &quot;Hello, ${&quot;$&quot;}{s}&quot; else &quot;Bye&quot;)\"\n        errorLine2=\"                                                ~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"833\"\n            column=\"49\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"40\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"50\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"100\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"110\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"159\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"168\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"217\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"226\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"277\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"287\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"332\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"343\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"388\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"399\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"431\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"461\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"472\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"493\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"502\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"521\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"530\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"              |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                    ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"553\"\n            column=\"21\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"              |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                    ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"564\"\n            column=\"21\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"587\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"598\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"642\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"652\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"675\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"686\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"724\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"735\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"774\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"786\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"824\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"835\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"864\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"874\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"905\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"915\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"946\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"956\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"987\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"997\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1026\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1036\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1059\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1069\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1100\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1110\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1141\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1151\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1183\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1194\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1238\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1249\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1295\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1307\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1330\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1340\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1366\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1379\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1404\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1416\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1441\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1453\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1477\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1488\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1533\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1544\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1589\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1600\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1623\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1633\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1655\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1665\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                |}&quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                      ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1687\"\n            column=\"23\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `java()` test file construction\"\n        errorLine1=\"                &quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                    ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1715\"\n            column=\"21\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"                &quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                    ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1730\"\n            column=\"21\"/>\n    </issue>\n\n    <issue\n        id=\"LintImplTrimIndent\"\n        message=\"No need to call `.trimMargin()` in issue registration strings; they are already trimmed by indent by lint when displaying to users. Instead, call `.indented()` on the surrounding `kotlin()` test file construction\"\n        errorLine1=\"              &quot;&quot;&quot;.trimMargin()\"\n        errorLine2=\"                  ~~~~~~~~~~~~\">\n        <location\n            file=\"src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt\"\n            line=\"1767\"\n            column=\"19\"/>\n    </issue>\n\n    <issue\n        id=\"UElementAsPsi\"\n        message=\"Do not use `UElement` as `PsiElement`\"\n        errorLine1=\"    if (expression is PsiMethodCallExpression) {\"\n        errorLine2=\"        ~~~~~~~~~~\">\n        <location\n            file=\"src/main/java/timber/lint/WrongTimberUsageDetector.kt\"\n            line=\"283\"\n            column=\"9\"/>\n    </issue>\n\n    <issue\n        id=\"UElementAsPsi\"\n        message=\"Do not use `UElement` as `PsiElement`\"\n        errorLine1=\"    } else if (expression is PsiLiteralExpression) {\"\n        errorLine2=\"               ~~~~~~~~~~\">\n        <location\n            file=\"src/main/java/timber/lint/WrongTimberUsageDetector.kt\"\n            line=\"290\"\n            column=\"16\"/>\n    </issue>\n\n</issues>\n"
  },
  {
    "path": "timber-lint/src/main/java/timber/lint/TimberIssueRegistry.kt",
    "content": "package timber.lint\n\nimport com.android.tools.lint.client.api.IssueRegistry\nimport com.android.tools.lint.client.api.Vendor\nimport com.android.tools.lint.detector.api.CURRENT_API\nimport com.android.tools.lint.detector.api.Issue\nimport com.google.auto.service.AutoService\n\n@Suppress(\"UnstableApiUsage\", \"unused\")\n@AutoService(value = [IssueRegistry::class])\nclass TimberIssueRegistry : IssueRegistry() {\n  override val issues: List<Issue>\n    get() = WrongTimberUsageDetector.issues.asList()\n\n  override val api: Int\n    get() = CURRENT_API\n\n  /** works with Studio 4.0 or later; see [com.android.tools.lint.detector.api.describeApi] */\n  override val minApi: Int\n    get() = 7\n\n  override val vendor =\n    Vendor(\n      vendorName = \"JakeWharton/timber\",\n      identifier = \"com.jakewharton.timber:timber:{version}\",\n      feedbackUrl = \"https://github.com/JakeWharton/timber/issues\",\n    )\n}\n"
  },
  {
    "path": "timber-lint/src/main/java/timber/lint/WrongTimberUsageDetector.kt",
    "content": "package timber.lint\n\nimport com.android.tools.lint.checks.StringFormatDetector\nimport com.android.tools.lint.client.api.JavaEvaluator\nimport com.android.tools.lint.client.api.TYPE_BOOLEAN\nimport com.android.tools.lint.client.api.TYPE_BOOLEAN_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_BYTE\nimport com.android.tools.lint.client.api.TYPE_BYTE_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_CHAR\nimport com.android.tools.lint.client.api.TYPE_DOUBLE\nimport com.android.tools.lint.client.api.TYPE_DOUBLE_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_FLOAT\nimport com.android.tools.lint.client.api.TYPE_FLOAT_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_INT\nimport com.android.tools.lint.client.api.TYPE_INTEGER_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_LONG\nimport com.android.tools.lint.client.api.TYPE_LONG_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_NULL\nimport com.android.tools.lint.client.api.TYPE_OBJECT\nimport com.android.tools.lint.client.api.TYPE_SHORT\nimport com.android.tools.lint.client.api.TYPE_SHORT_WRAPPER\nimport com.android.tools.lint.client.api.TYPE_STRING\nimport com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS\nimport com.android.tools.lint.detector.api.Category.Companion.MESSAGES\nimport com.android.tools.lint.detector.api.ConstantEvaluator.evaluateString\nimport com.android.tools.lint.detector.api.Detector\nimport com.android.tools.lint.detector.api.Detector.UastScanner\nimport com.android.tools.lint.detector.api.Implementation\nimport com.android.tools.lint.detector.api.Incident\nimport com.android.tools.lint.detector.api.Issue\nimport com.android.tools.lint.detector.api.JavaContext\nimport com.android.tools.lint.detector.api.LintFix\nimport com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE\nimport com.android.tools.lint.detector.api.Severity.ERROR\nimport com.android.tools.lint.detector.api.Severity.WARNING\nimport com.android.tools.lint.detector.api.isKotlin\nimport com.android.tools.lint.detector.api.isString\nimport com.android.tools.lint.detector.api.minSdkLessThan\nimport com.android.tools.lint.detector.api.skipParentheses\nimport com.intellij.psi.PsiClassType\nimport com.intellij.psi.PsiLiteralExpression\nimport com.intellij.psi.PsiMethod\nimport com.intellij.psi.PsiMethodCallExpression\nimport com.intellij.psi.PsiType\nimport com.intellij.psi.PsiTypes\nimport java.lang.Byte\nimport java.lang.Double\nimport java.lang.Float\nimport java.lang.IllegalStateException\nimport java.lang.Long\nimport java.lang.Short\nimport java.util.Calendar\nimport java.util.Date\nimport java.util.regex.Pattern\nimport kotlin.Any\nimport kotlin.Boolean\nimport kotlin.Number\nimport kotlin.String\nimport kotlin.Throwable\nimport kotlin.arrayOf\nimport org.jetbrains.uast.UBinaryExpression\nimport org.jetbrains.uast.UCallExpression\nimport org.jetbrains.uast.UElement\nimport org.jetbrains.uast.UExpression\nimport org.jetbrains.uast.UIfExpression\nimport org.jetbrains.uast.ULiteralExpression\nimport org.jetbrains.uast.UMethod\nimport org.jetbrains.uast.UQualifiedReferenceExpression\nimport org.jetbrains.uast.UastBinaryOperator\nimport org.jetbrains.uast.evaluateString\nimport org.jetbrains.uast.isInjectionHost\nimport org.jetbrains.uast.util.isMethodCall\n\nclass WrongTimberUsageDetector : Detector(), UastScanner {\n  override fun getApplicableMethodNames() = listOf(\"tag\", \"format\", \"v\", \"d\", \"i\", \"w\", \"e\", \"wtf\")\n\n  override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {\n    val methodName = node.methodName\n    val evaluator = context.evaluator\n\n    if (\n      \"format\" == methodName &&\n        (evaluator.isMemberInClass(method, \"java.lang.String\") ||\n          evaluator.isMemberInClass(method, \"kotlin.text.StringsKt__StringsJVMKt\"))\n    ) {\n      checkNestedStringFormat(context, node)\n      return\n    }\n    if (\"tag\" == methodName && evaluator.isMemberInClass(method, \"timber.log.Timber\")) {\n      checkTagLengthIfMinSdkLessThan26(context, node)\n    }\n    if (evaluator.isMemberInClass(method, \"android.util.Log\")) {\n      context.report(\n        Incident(\n          issue = ISSUE_LOG,\n          scope = node,\n          location = context.getLocation(node),\n          message = \"Using 'Log' instead of 'Timber'\",\n          fix = quickFixIssueLog(node),\n        )\n      )\n      return\n    }\n    // Handles Timber.X(..) and Timber.tag(..).X(..) where X in (v|d|i|w|e|wtf).\n    if (isTimberLogMethod(method, evaluator)) {\n      checkMethodArguments(context, node)\n      checkFormatArguments(context, node)\n      checkExceptionLogging(context, node)\n    }\n  }\n\n  private fun isTimberLogMethod(method: PsiMethod, evaluator: JavaEvaluator): Boolean {\n    return evaluator.isMemberInClass(method, \"timber.log.Timber\") ||\n      evaluator.isMemberInClass(method, \"timber.log.Timber.Companion\") ||\n      evaluator.isMemberInClass(method, \"timber.log.Timber.Tree\")\n  }\n\n  private fun checkNestedStringFormat(context: JavaContext, call: UCallExpression) {\n    var current: UElement? = call\n    while (true) {\n      current = skipParentheses(current!!.uastParent)\n      if (current == null || current is UMethod) {\n        // Reached AST root or code block node; String.format not inside Timber.X(..).\n        return\n      }\n      if (current.isMethodCall()) {\n        val psiMethod = (current as UCallExpression).resolve()\n        if (\n          psiMethod != null &&\n            Pattern.matches(TIMBER_TREE_LOG_METHOD_REGEXP, psiMethod.name) &&\n            isTimberLogMethod(psiMethod, context.evaluator)\n        ) {\n          context.report(\n            Incident(\n              issue = ISSUE_FORMAT,\n              scope = call,\n              location = context.getLocation(call),\n              message = \"Using 'String#format' inside of 'Timber'\",\n              fix = quickFixIssueFormat(call),\n            )\n          )\n          return\n        }\n      }\n    }\n  }\n\n  private fun checkTagLengthIfMinSdkLessThan26(context: JavaContext, call: UCallExpression) {\n    val argument = call.valueArguments[0]\n    val tag = evaluateString(context, argument, true)\n    if (tag != null && tag.length > 23) {\n      context.report(\n        Incident(\n          issue = ISSUE_TAG_LENGTH,\n          scope = argument,\n          location = context.getLocation(argument),\n          message = \"The logging tag can be at most 23 characters, was ${tag.length} ($tag)\",\n          fix = quickFixIssueTagLength(argument, tag),\n        ),\n        // As of API 26, Log tags are no longer limited to 23 chars.\n        constraint = minSdkLessThan(26),\n      )\n    }\n  }\n\n  private fun checkFormatArguments(context: JavaContext, call: UCallExpression) {\n    val arguments = call.valueArguments\n    val numArguments = arguments.size\n    if (numArguments == 0) {\n      return\n    }\n\n    var startIndexOfArguments = 1\n    var formatStringArg = arguments[0]\n    if (isSubclassOf(context, formatStringArg, Throwable::class.java)) {\n      if (numArguments == 1) {\n        return\n      }\n      formatStringArg = arguments[1]\n      startIndexOfArguments++\n    }\n\n    val formatString =\n      evaluateString(context, formatStringArg, false)\n        ?: return // We passed for example a method call\n\n    val formatArgumentCount = getFormatArgumentCount(formatString)\n    val passedArgCount = numArguments - startIndexOfArguments\n    if (formatArgumentCount < passedArgCount) {\n      context.report(\n        Incident(\n          issue = ISSUE_ARG_COUNT,\n          scope = call,\n          location = context.getLocation(call),\n          message =\n            \"Wrong argument count, format string `${formatString}` requires `${formatArgumentCount}` but format call supplies `${passedArgCount}`\",\n        )\n      )\n      return\n    }\n\n    if (formatArgumentCount == 0) {\n      return\n    }\n\n    val types = getStringArgumentTypes(formatString)\n    var argument: UExpression? = null\n    var argumentIndex = startIndexOfArguments\n    var valid: Boolean\n    for (i in types.indices) {\n      val formatType = types[i]\n      if (argumentIndex != numArguments) {\n        argument = arguments[argumentIndex++]\n      } else {\n        context.report(\n          Incident(\n            issue = ISSUE_ARG_COUNT,\n            scope = call,\n            location = context.getLocation(call),\n            message =\n              \"Wrong argument count, format string `${formatString}` requires `${formatArgumentCount}` but format call supplies `${passedArgCount}`\",\n          )\n        )\n      }\n\n      val type = getType(argument) ?: continue\n      val last = formatType.last()\n      if (formatType.length >= 2 && formatType[formatType.length - 2].lowercaseChar() == 't') {\n        // Date time conversion.\n        when (last) {\n          'H',\n          'I',\n          'k',\n          'l',\n          'M',\n          'S',\n          'L',\n          'N',\n          'p',\n          'z',\n          'Z',\n          's',\n          'Q', // time\n          'B',\n          'b',\n          'h',\n          'A',\n          'a',\n          'C',\n          'Y',\n          'y',\n          'j',\n          'm',\n          'd',\n          'e', // date\n          'R',\n          'T',\n          'r',\n          'D',\n          'F',\n          'c' -> { // date/time\n            valid =\n              type == Integer.TYPE ||\n                type == Calendar::class.java ||\n                type == Date::class.java ||\n                type == Long.TYPE\n            if (!valid) {\n              context.report(\n                Incident(\n                  issue = ISSUE_ARG_TYPES,\n                  scope = call,\n                  location = context.getLocation(argument),\n                  message =\n                    \"Wrong argument type for date formatting argument '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)\",\n                )\n              )\n            }\n          }\n          else -> {\n            context.report(\n              Incident(\n                issue = ISSUE_FORMAT,\n                scope = call,\n                location = context.getLocation(argument),\n                message =\n                  \"Wrong suffix for date format '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)\",\n              )\n            )\n          }\n        }\n        continue\n      }\n\n      valid =\n        when (last) {\n          'b',\n          'B' -> type == java.lang.Boolean.TYPE\n          'x',\n          'X',\n          'd',\n          'o',\n          'e',\n          'E',\n          'f',\n          'g',\n          'G',\n          'a',\n          'A' -> {\n            type == Integer.TYPE ||\n              type == Float.TYPE ||\n              type == Double.TYPE ||\n              type == Long.TYPE ||\n              type == Byte.TYPE ||\n              type == Short.TYPE\n          }\n          'c',\n          'C' -> type == Character.TYPE\n          'h',\n          'H' -> type != java.lang.Boolean.TYPE && !Number::class.java.isAssignableFrom(type)\n          's',\n          'S' -> true\n          else -> true\n        }\n      if (!valid) {\n        context.report(\n          Incident(\n            issue = ISSUE_ARG_TYPES,\n            scope = call,\n            location = context.getLocation(argument),\n            message =\n              \"Wrong argument type for formatting argument '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)\",\n          )\n        )\n      }\n    }\n  }\n\n  private fun getType(expression: UExpression?): Class<*>? {\n    if (expression == null) {\n      return null\n    }\n    if (expression is PsiMethodCallExpression) {\n      val call = expression as PsiMethodCallExpression\n      val method = call.resolveMethod() ?: return null\n      val methodName = method.name\n      if (methodName == GET_STRING_METHOD) {\n        return String::class.java\n      }\n    } else if (expression is PsiLiteralExpression) {\n      val literalExpression = expression as PsiLiteralExpression\n      val expressionType = literalExpression.type\n      when {\n        isString(expressionType!!) -> return String::class.java\n        expressionType === PsiTypes.intType() -> return Integer.TYPE\n        expressionType === PsiTypes.floatType() -> return Float.TYPE\n        expressionType === PsiTypes.charType() -> return Character.TYPE\n        expressionType === PsiTypes.booleanType() -> return java.lang.Boolean.TYPE\n        expressionType === PsiTypes.nullType() -> return Any::class.java\n      }\n    }\n\n    val type = expression.getExpressionType()\n    if (type != null) {\n      val typeClass = getTypeClass(type)\n      return typeClass ?: Any::class.java\n    }\n\n    return null\n  }\n\n  private fun getTypeClass(type: PsiType?): Class<*>? {\n    return when (type?.canonicalText) {\n      null -> null\n      TYPE_STRING,\n      \"String\" -> String::class.java\n      TYPE_INT -> Integer.TYPE\n      TYPE_BOOLEAN -> java.lang.Boolean.TYPE\n      TYPE_NULL -> Object::class.java\n      TYPE_LONG -> Long.TYPE\n      TYPE_FLOAT -> Float.TYPE\n      TYPE_DOUBLE -> Double.TYPE\n      TYPE_CHAR -> Character.TYPE\n      TYPE_OBJECT -> null\n      TYPE_INTEGER_WRAPPER,\n      TYPE_SHORT_WRAPPER,\n      TYPE_BYTE_WRAPPER,\n      TYPE_LONG_WRAPPER -> Integer.TYPE\n      TYPE_FLOAT_WRAPPER,\n      TYPE_DOUBLE_WRAPPER -> Float.TYPE\n      TYPE_BOOLEAN_WRAPPER -> java.lang.Boolean.TYPE\n      TYPE_BYTE -> Byte.TYPE\n      TYPE_SHORT -> Short.TYPE\n      \"Date\",\n      \"java.util.Date\" -> Date::class.java\n      \"Calendar\",\n      \"java.util.Calendar\" -> Calendar::class.java\n      \"BigDecimal\",\n      \"java.math.BigDecimal\" -> Float.TYPE\n      \"BigInteger\",\n      \"java.math.BigInteger\" -> Integer.TYPE\n      else -> null\n    }\n  }\n\n  private fun isSubclassOf(context: JavaContext, expression: UExpression, cls: Class<*>): Boolean {\n    val expressionType = expression.getExpressionType()\n    if (expressionType is PsiClassType) {\n      return context.evaluator.extendsClass(expressionType.resolve(), cls.name, false)\n    }\n    return false\n  }\n\n  private fun getStringArgumentTypes(formatString: String): List<String> {\n    val types = mutableListOf<String>()\n    val matcher = StringFormatDetector.FORMAT.matcher(formatString)\n    var index = 0\n    var prevIndex = 0\n\n    while (true) {\n      if (matcher.find(index)) {\n        val matchStart = matcher.start()\n        while (prevIndex < matchStart) {\n          val c = formatString[prevIndex]\n          if (c == '\\\\') {\n            prevIndex++\n          }\n          prevIndex++\n        }\n        if (prevIndex > matchStart) {\n          index = prevIndex\n          continue\n        }\n\n        index = matcher.end()\n        val str = formatString.substring(matchStart, matcher.end())\n        if (\"%%\" == str || \"%n\" == str) {\n          continue\n        }\n        val time = matcher.group(5)\n        types +=\n          if (\"t\".equals(time, ignoreCase = true)) {\n            time + matcher.group(6)\n          } else {\n            matcher.group(6)\n          }\n      } else {\n        break\n      }\n    }\n    return types\n  }\n\n  private fun getFormatArgumentCount(s: String): Int {\n    val matcher = StringFormatDetector.FORMAT.matcher(s)\n    var index = 0\n    var prevIndex = 0\n    var nextNumber = 1\n    var max = 0\n    while (true) {\n      if (matcher.find(index)) {\n        val value = matcher.group(6)\n        if (\"%\" == value || \"n\" == value) {\n          index = matcher.end()\n          continue\n        }\n        val matchStart = matcher.start()\n        while (prevIndex < matchStart) {\n          val c = s[prevIndex]\n          if (c == '\\\\') {\n            prevIndex++\n          }\n          prevIndex++\n        }\n        if (prevIndex > matchStart) {\n          index = prevIndex\n          continue\n        }\n\n        var number: Int\n        var numberString = matcher.group(1)\n        if (numberString != null) {\n          // Strip off trailing $\n          numberString = numberString.substring(0, numberString.length - 1)\n          number = numberString.toInt()\n          nextNumber = number + 1\n        } else {\n          number = nextNumber++\n        }\n        if (number > max) {\n          max = number\n        }\n        index = matcher.end()\n      } else {\n        break\n      }\n    }\n    return max\n  }\n\n  private fun checkMethodArguments(context: JavaContext, call: UCallExpression) {\n    call.valueArguments.forEachIndexed loop@{ i, argument ->\n      if (checkElement(context, call, argument)) return@loop\n\n      if (i > 0 && isSubclassOf(context, argument, Throwable::class.java)) {\n        context.report(\n          Incident(\n            issue = ISSUE_THROWABLE,\n            scope = call,\n            location = context.getLocation(call),\n            message = \"Throwable should be first argument\",\n            fix = quickFixIssueThrowable(call, call.valueArguments, argument),\n          )\n        )\n      }\n    }\n  }\n\n  private fun checkExceptionLogging(context: JavaContext, call: UCallExpression) {\n    val arguments = call.valueArguments\n    val numArguments = arguments.size\n\n    // Find the throwable and message arguments by their type, not by their position.\n    val throwableArgument = arguments.firstOrNull {\n      isSubclassOf(context, it, Throwable::class.java)\n    }\n\n    // Find an argument that is either a String OR a literal null expression.\n    val messageArgument = arguments.firstOrNull {\n      val type = it.getExpressionType()\n      (type != null && isString(type)) || (it is ULiteralExpression && it.isNull)\n    }\n\n    // Handles overloads like Timber.d(t, \"message\").\n    if (throwableArgument != null && messageArgument != null) {\n      // Check for the common mistake of explicitly logging the exception's own message.\n      if (isLoggingExceptionMessage(context, messageArgument)) {\n        context.report(\n          Incident(\n            issue = ISSUE_EXCEPTION_LOGGING,\n            scope = messageArgument,\n            location = context.getLocation(call),\n            message = \"Explicitly logging exception message is redundant\",\n            fix = quickFixRemoveRedundantArgument(messageArgument),\n          )\n        )\n        return\n      }\n\n      // Check if the argument is a literal null, which is a clear issue.\n      if (messageArgument is ULiteralExpression && messageArgument.isNull) {\n        context.report(\n          Incident(\n            issue = ISSUE_EXCEPTION_LOGGING,\n            scope = messageArgument,\n            location = context.getLocation(call),\n            message = \"Use single-argument log method instead of null/empty message\",\n            fix = quickFixRemoveRedundantArgument(messageArgument),\n          )\n        )\n        return\n      }\n\n      // If it's not null, then try to evaluate it as a string.\n      val messageValue = evaluateString(context, messageArgument, true)\n\n      // If we could determine the string's value (i.e., it's a literal or constant)...\n      if (messageValue != null) {\n        // ...then we can safely check if it's empty and report an issue.\n        if (messageValue.isEmpty()) {\n          context.report(\n            Incident(\n              issue = ISSUE_EXCEPTION_LOGGING,\n              scope = messageArgument,\n              location = context.getLocation(call),\n              message = \"Use single-argument log method instead of null/empty message\",\n              fix = quickFixRemoveRedundantArgument(messageArgument),\n            )\n          )\n        }\n      }\n      // If messageValue is null, the argument is a variable or method call. We intentionally\n      // do nothing in this case to avoid false positives.\n\n      // Handles single-argument overloads like Timber.d(\"message\").\n    } else if (numArguments == 1 && throwableArgument == null && messageArgument != null) {\n      if (isLoggingExceptionMessage(context, messageArgument)) {\n        context.report(\n          Incident(\n            issue = ISSUE_EXCEPTION_LOGGING,\n            scope = messageArgument,\n            location = context.getLocation(call),\n            message = \"Explicitly logging exception message is redundant\",\n            fix = quickFixReplaceMessageWithThrowable(messageArgument),\n          )\n        )\n      }\n    }\n  }\n\n  private fun isLoggingExceptionMessage(context: JavaContext, arg: UExpression): Boolean {\n    if (arg !is UQualifiedReferenceExpression) {\n      return false\n    }\n\n    val psi = arg.sourcePsi\n    if (psi != null && isKotlin(psi.language)) {\n      return isPropertyOnSubclassOf(context, arg, \"message\", Throwable::class.java)\n    }\n\n    val selector = arg.selector\n\n    // what other UExpressions could be a selector?\n    return if (selector !is UCallExpression) {\n      false\n    } else\n      isCallFromMethodInSubclassOf(\n        context = context,\n        call = selector,\n        methodName = \"getMessage\",\n        classType = Throwable::class.java,\n      )\n  }\n\n  private fun isCallFromMethodInSubclassOf(\n    context: JavaContext,\n    call: UCallExpression,\n    methodName: String,\n    classType: Class<*>,\n  ): Boolean {\n    val method = call.resolve()\n    return method != null &&\n      methodName == call.methodName &&\n      context.evaluator.isMemberInSubClassOf(method, classType.canonicalName, false)\n  }\n\n  private fun isPropertyOnSubclassOf(\n    context: JavaContext,\n    expression: UQualifiedReferenceExpression,\n    propertyName: String,\n    classType: Class<*>,\n  ): Boolean {\n    return isSubclassOf(context, expression.receiver, classType) &&\n      expression.selector.asSourceString() == propertyName\n  }\n\n  private fun checkElement(\n    context: JavaContext,\n    call: UCallExpression,\n    element: UElement?,\n  ): Boolean {\n    if (element is UBinaryExpression) {\n      val operator = element.operator\n      if (operator === UastBinaryOperator.PLUS || operator === UastBinaryOperator.PLUS_ASSIGN) {\n        val argumentType = getType(element)\n        if (argumentType == String::class.java) {\n          if (element.leftOperand.isInjectionHost() && element.rightOperand.isInjectionHost()) {\n            return false\n          }\n          context.report(\n            Incident(\n              issue = ISSUE_BINARY,\n              scope = call,\n              location = context.getLocation(element),\n              message = \"Replace String concatenation with Timber's string formatting\",\n              fix = quickFixIssueBinary(element),\n            )\n          )\n          return true\n        }\n      }\n    } else if (element is UIfExpression) {\n      return checkConditionalUsage(context, call, element)\n    }\n    return false\n  }\n\n  private fun checkConditionalUsage(\n    context: JavaContext,\n    call: UCallExpression,\n    element: UElement,\n  ): Boolean {\n    return if (element is UIfExpression) {\n      if (checkElement(context, call, element.thenExpression)) {\n        false\n      } else {\n        checkElement(context, call, element.elseExpression)\n      }\n    } else {\n      false\n    }\n  }\n\n  private fun quickFixIssueLog(logCall: UCallExpression): LintFix {\n    val arguments = logCall.valueArguments\n    val methodName = logCall.methodName\n    val tag = arguments[0]\n\n    // 1st suggestion respects author's tag preference.\n    // 2nd suggestion drops it (Timber defaults to calling class name).\n    var fixSource1 = \"Timber.tag(${tag.asSourceString()}).\"\n    var fixSource2 = \"Timber.\"\n\n    when (arguments.size) {\n      2 -> {\n        val msgOrThrowable = arguments[1]\n        fixSource1 += \"$methodName(${msgOrThrowable.asSourceString()})\"\n        fixSource2 += \"$methodName(${msgOrThrowable.asSourceString()})\"\n      }\n      3 -> {\n        val msg = arguments[1]\n        val throwable = arguments[2]\n        fixSource1 += \"$methodName(${throwable.sourcePsi?.text}, ${msg.asSourceString()})\"\n        fixSource2 += \"$methodName(${throwable.sourcePsi?.text}, ${msg.asSourceString()})\"\n      }\n      else -> {\n        throw IllegalStateException(\"android.util.Log overloads should have 2 or 3 arguments\")\n      }\n    }\n\n    val logCallSource = logCall.uastParent!!.sourcePsi?.text\n    return fix()\n      .group()\n      .add(\n        fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource1).build()\n      )\n      .add(\n        fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource2).build()\n      )\n      .build()\n  }\n\n  private fun quickFixIssueFormat(stringFormatCall: UCallExpression): LintFix {\n    // Handles:\n    // 1) String.format(..)\n    // 2) format(...) [static import]\n    val callReceiver = stringFormatCall.receiver\n    var callSourceString = if (callReceiver == null) \"\" else \"${callReceiver.asSourceString()}.\"\n    callSourceString += stringFormatCall.methodName\n\n    return fix()\n      .name(\"Remove String.format(...)\")\n      .composite() //\n      // Delete closing parenthesis of String.format(...)\n      .add(fix().replace().pattern(\"$callSourceString\\\\(.*(\\\\))\").with(\"\").build())\n      // Delete \"String.format(\"\n      .add(fix().replace().text(\"$callSourceString(\").with(\"\").build())\n      .build()\n  }\n\n  private fun quickFixIssueThrowable(\n    call: UCallExpression,\n    arguments: List<UExpression>,\n    throwable: UExpression,\n  ): LintFix {\n    val rearrangedArgs = buildString {\n      append(throwable.asSourceString())\n      arguments.forEach { arg ->\n        if (arg !== throwable) {\n          append(\", ${arg.asSourceString()}\")\n        }\n      }\n    }\n    return fix()\n      .replace()\n      .pattern(\"\\\\.\" + call.methodName + \"\\\\((.*)\\\\)\")\n      .with(rearrangedArgs)\n      .build()\n  }\n\n  private fun quickFixIssueBinary(binaryExpression: UBinaryExpression): LintFix {\n    val leftOperand = binaryExpression.leftOperand\n    val rightOperand = binaryExpression.rightOperand\n    val isLeftLiteral = leftOperand.isInjectionHost()\n    val isRightLiteral = rightOperand.isInjectionHost()\n\n    // \"a\" + \"b\" => \"ab\"\n    if (isLeftLiteral && isRightLiteral) {\n      return fix()\n        .replace()\n        .text(binaryExpression.asSourceString())\n        .with(\"\\\"${binaryExpression.evaluateString()}\\\"\")\n        .build()\n    }\n\n    val args: String =\n      when {\n        isLeftLiteral -> {\n          \"\\\"${leftOperand.evaluateString()}%s\\\", ${rightOperand.asSourceString()}\"\n        }\n        isRightLiteral -> {\n          \"\\\"%s${rightOperand.evaluateString()}\\\", ${leftOperand.asSourceString()}\"\n        }\n        else -> {\n          \"\\\"%s%s\\\", ${leftOperand.asSourceString()}, ${rightOperand.asSourceString()}\"\n        }\n      }\n    return fix().replace().text(binaryExpression.asSourceString()).with(args).build()\n  }\n\n  private fun quickFixIssueTagLength(argument: UExpression, tag: String): LintFix {\n    val numCharsToTrim = tag.length - 23\n    return fix()\n      .replace()\n      .name(\"Strip last \" + if (numCharsToTrim == 1) \"char\" else \"$numCharsToTrim chars\")\n      .text(argument.asSourceString())\n      .with(\"\\\"${tag.substring(0, 23)}\\\"\")\n      .build()\n  }\n\n  private fun quickFixRemoveRedundantArgument(arg: UExpression): LintFix {\n    return fix()\n      .replace()\n      .name(\"Remove redundant argument\")\n      .text(\", ${arg.asSourceString()}\")\n      .with(\"\")\n      .build()\n  }\n\n  private fun quickFixReplaceMessageWithThrowable(arg: UExpression): LintFix {\n    // guaranteed based on callers of this method\n    val receiver = (arg as UQualifiedReferenceExpression).receiver\n    return fix()\n      .replace()\n      .name(\"Replace message with throwable\")\n      .text(arg.asSourceString())\n      .with(receiver.asSourceString())\n      .build()\n  }\n\n  companion object {\n    private const val GET_STRING_METHOD = \"getString\"\n    private const val TIMBER_TREE_LOG_METHOD_REGEXP = \"(v|d|i|w|e|wtf)\"\n\n    val ISSUE_LOG =\n      Issue.create(\n        id = \"LogNotTimber\",\n        briefDescription = \"Logging call to Log instead of Timber\",\n        explanation =\n          \"Since Timber is included in the project, it is likely that calls to Log should instead be going to Timber.\",\n        category = MESSAGES,\n        priority = 5,\n        severity = WARNING,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_FORMAT =\n      Issue.create(\n        id = \"StringFormatInTimber\",\n        briefDescription = \"Logging call with Timber contains String#format()\",\n        explanation =\n          \"Since Timber handles String.format automatically, you may not use String#format().\",\n        category = MESSAGES,\n        priority = 5,\n        severity = WARNING,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_THROWABLE =\n      Issue.create(\n        id = \"ThrowableNotAtBeginning\",\n        briefDescription = \"Exception in Timber not at the beginning\",\n        explanation = \"In Timber you have to pass a Throwable at the beginning of the call.\",\n        category = MESSAGES,\n        priority = 5,\n        severity = WARNING,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_BINARY =\n      Issue.create(\n        id = \"BinaryOperationInTimber\",\n        briefDescription = \"Use String#format()\",\n        explanation =\n          \"Since Timber handles String#format() automatically, use this instead of String concatenation.\",\n        category = MESSAGES,\n        priority = 5,\n        severity = WARNING,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_ARG_COUNT =\n      Issue.create(\n        id = \"TimberArgCount\",\n        briefDescription = \"Formatting argument types incomplete or inconsistent\",\n        explanation =\n          \"When a formatted string takes arguments, you need to pass at least that amount of arguments to the formatting call.\",\n        category = MESSAGES,\n        priority = 9,\n        severity = ERROR,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_ARG_TYPES =\n      Issue.create(\n        id = \"TimberArgTypes\",\n        briefDescription = \"Formatting string doesn't match passed arguments\",\n        explanation =\n          \"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.\",\n        category = MESSAGES,\n        priority = 9,\n        severity = ERROR,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_TAG_LENGTH =\n      Issue.create(\n        id = \"TimberTagLength\",\n        briefDescription = \"Too Long Log Tags\",\n        explanation = \"Log tags are only allowed to be at most\" + \" 23 tag characters long.\",\n        category = CORRECTNESS,\n        priority = 5,\n        severity = ERROR,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n    val ISSUE_EXCEPTION_LOGGING =\n      Issue.create(\n        id = \"TimberExceptionLogging\",\n        briefDescription = \"Exception Logging\",\n        explanation =\n          \"Explicitly including the exception message is redundant when supplying an exception to log.\",\n        category = CORRECTNESS,\n        priority = 3,\n        severity = WARNING,\n        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),\n      )\n\n    val issues =\n      arrayOf(\n        ISSUE_LOG,\n        ISSUE_FORMAT,\n        ISSUE_THROWABLE,\n        ISSUE_BINARY,\n        ISSUE_ARG_COUNT,\n        ISSUE_ARG_TYPES,\n        ISSUE_TAG_LENGTH,\n        ISSUE_EXCEPTION_LOGGING,\n      )\n  }\n}\n"
  },
  {
    "path": "timber-lint/src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt",
    "content": "package timber.lint\n\nimport com.android.tools.lint.checks.infrastructure.TestFiles.java\nimport com.android.tools.lint.checks.infrastructure.TestFiles.kotlin\nimport com.android.tools.lint.checks.infrastructure.TestFiles.manifest\nimport com.android.tools.lint.checks.infrastructure.TestLintTask.lint\nimport com.android.tools.lint.checks.infrastructure.TestMode\nimport org.junit.Test\nimport timber.lint.WrongTimberUsageDetector.Companion.issues\n\nclass WrongTimberUsageDetectorTest {\n  private val TIMBER_STUB =\n    kotlin(\n      \"\"\"\n      |package timber.log\n      |class Timber private constructor() {\n      |  private companion object {\n      |    @JvmStatic fun d(message: String?, vararg args: Any?) {}\n      |    @JvmStatic fun d(t: Throwable?, message: String?, vararg args: Any?) {}\n      |    @JvmStatic fun tag(tag: String) = Tree()\n      |  }\n      |  open class Tree {\n      |    open fun d(message: String?, vararg args: Any?) {}\n      |    open fun d(t: Throwable?, message: String?, vararg args: Any?) {}\n      |  }\n      |}\n      \"\"\"\n        .trimMargin()\n    )\n\n  @Test\n  fun usingAndroidLogWithTwoArguments() {\n    lint()\n      .files(\n        java(\n          \"\"\"\n          |package foo;\n          |import android.util.Log;\n          |public class Example {\n          |  public void log() {\n          |    Log.d(\"TAG\", \"msg\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import android.util.Log\n          |class Example {\n          |  fun log() {\n          |    Log.d(\"TAG\", \"msg\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    Log.d(\"TAG\", \"msg\");\n        |    ~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    Log.d(\"TAG\", \"msg\")\n        |    ~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 5: Replace with Timber.tag(\"TAG\").d(\"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\");\n        |+     Timber.tag(\"TAG\").d(\"msg\");\n        |Fix for src/foo/Example.java line 5: Replace with Timber.d(\"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\");\n        |+     Timber.d(\"msg\");\n        |Fix for src/foo/Example.kt line 5: Replace with Timber.tag(\"TAG\").d(\"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\")\n        |+     Timber.tag(\"TAG\").d(\"msg\")\n        |Fix for src/foo/Example.kt line 5: Replace with Timber.d(\"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\")\n        |+     Timber.d(\"msg\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun usingAndroidLogWithThreeArguments() {\n    lint()\n      .files(\n        java(\n          \"\"\"\n          |package foo;\n          |import android.util.Log;\n          |public class Example {\n          |  public void log() {\n          |    Log.d(\"TAG\", \"msg\", new Exception());\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import android.util.Log\n          |class Example {\n          |  fun log() {\n          |    Log.d(\"TAG\", \"msg\", Exception())\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    Log.d(\"TAG\", \"msg\", new Exception());\n        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    Log.d(\"TAG\", \"msg\", Exception())\n        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 5: Replace with Timber.tag(\"TAG\").d(new Exception(), \"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\", new Exception());\n        |+     Timber.tag(\"TAG\").d(new Exception(), \"msg\");\n        |Fix for src/foo/Example.java line 5: Replace with Timber.d(new Exception(), \"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\", new Exception());\n        |+     Timber.d(new Exception(), \"msg\");\n        |Fix for src/foo/Example.kt line 5: Replace with Timber.tag(\"TAG\").d(Exception(), \"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\", Exception())\n        |+     Timber.tag(\"TAG\").d(Exception(), \"msg\")\n        |Fix for src/foo/Example.kt line 5: Replace with Timber.d(Exception(), \"msg\"):\n        |@@ -5 +5\n        |-     Log.d(\"TAG\", \"msg\", Exception())\n        |+     Timber.d(Exception(), \"msg\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun usingFullyQualifiedAndroidLogWithTwoArguments() {\n    lint()\n      .files(\n        java(\n          \"\"\"\n          |package foo;\n          |public class Example {\n          |  public void log() {\n          |    android.util.Log.d(\"TAG\", \"msg\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |class Example {\n          |  fun log() {\n          |    android.util.Log.d(\"TAG\", \"msg\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    android.util.Log.d(\"TAG\", \"msg\");\n        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    android.util.Log.d(\"TAG\", \"msg\")\n        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 4: Replace with Timber.tag(\"TAG\").d(\"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\");\n        |+     Timber.tag(\"TAG\").d(\"msg\");\n        |Fix for src/foo/Example.java line 4: Replace with Timber.d(\"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\");\n        |+     Timber.d(\"msg\");\n        |Fix for src/foo/Example.kt line 4: Replace with Timber.tag(\"TAG\").d(\"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\")\n        |+     Timber.tag(\"TAG\").d(\"msg\")\n        |Fix for src/foo/Example.kt line 4: Replace with Timber.d(\"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\")\n        |+     Timber.d(\"msg\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun usingFullyQualifiedAndroidLogWithThreeArguments() {\n    lint()\n      .files(\n        java(\n          \"\"\"\n          |package foo;\n          |public class Example {\n          |  public void log() {\n          |    android.util.Log.d(\"TAG\", \"msg\", new Exception());\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |class Example {\n          |  fun log() {\n          |    android.util.Log.d(\"TAG\", \"msg\", Exception())\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    android.util.Log.d(\"TAG\", \"msg\", new Exception());\n        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]\n        |    android.util.Log.d(\"TAG\", \"msg\", Exception())\n        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 4: Replace with Timber.tag(\"TAG\").d(new Exception(), \"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\", new Exception());\n        |+     Timber.tag(\"TAG\").d(new Exception(), \"msg\");\n        |Fix for src/foo/Example.java line 4: Replace with Timber.d(new Exception(), \"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\", new Exception());\n        |+     Timber.d(new Exception(), \"msg\");\n        |Fix for src/foo/Example.kt line 4: Replace with Timber.tag(\"TAG\").d(Exception(), \"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\", Exception())\n        |+     Timber.tag(\"TAG\").d(Exception(), \"msg\")\n        |Fix for src/foo/Example.kt line 4: Replace with Timber.d(Exception(), \"msg\"):\n        |@@ -4 +4\n        |-     android.util.Log.d(\"TAG\", \"msg\", Exception())\n        |+     Timber.d(Exception(), \"msg\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun innerStringFormat() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(String.format(\"%s\", \"arg1\"));\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.d(String.format(\"%s\", \"arg1\"))\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.WHITESPACE)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |     Timber.d(String.format(\"%s\", \"arg1\"));\n        |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |     Timber.d(String.format(\"%s\", \"arg1\"))\n        |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 5: Remove String.format(...):\n        |@@ -5 +5\n        |-      Timber.d(String.format(\"%s\", \"arg1\"));\n        |+      Timber.d(\"%s\", \"arg1\");\n        |Fix for src/foo/Example.kt line 5: Remove String.format(...):\n        |@@ -5 +5\n        |-      Timber.d(String.format(\"%s\", \"arg1\"))\n        |+      Timber.d(\"%s\", \"arg1\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun innerStringFormatWithStaticImport() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |import static java.lang.String.format;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(format(\"%s\", \"arg1\"));\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |import java.lang.String.format\n          |class Example {\n          |  fun log() {\n          |     Timber.d(format(\"%s\", \"arg1\"))\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.PARENTHESIZED, TestMode.WHITESPACE)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |     Timber.d(format(\"%s\", \"arg1\"));\n        |              ~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |     Timber.d(format(\"%s\", \"arg1\"))\n        |              ~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 6: Remove String.format(...):\n        |@@ -6 +6\n        |-      Timber.d(format(\"%s\", \"arg1\"));\n        |+      Timber.d(\"%s\", \"arg1\");\n        |Fix for src/foo/Example.kt line 6: Remove String.format(...):\n        |@@ -6 +6\n        |-      Timber.d(format(\"%s\", \"arg1\"))\n        |+      Timber.d(\"%s\", \"arg1\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun innerStringFormatInNestedMethods() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(id(String.format(\"%s\", \"arg1\")));\n          |  }\n          |  private String id(String s) { return s; }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.d(id(String.format(\"%s\", \"arg1\")))\n          |  }\n          |  private fun id(s: String): String { return s }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |     Timber.d(id(String.format(\"%s\", \"arg1\")));\n        |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |     Timber.d(id(String.format(\"%s\", \"arg1\")))\n        |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun innerStringFormatInNestedAssignment() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |    String msg = null;\n          |    Timber.d(msg = String.format(\"msg\"));\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        // no kotlin equivalent, since nested assignments do not exist\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]\n        |    Timber.d(msg = String.format(\"msg\"));\n        |                   ~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 1 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun validStringFormatInCodeBlock() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |public class Example {\n          |  public void log() {\n          |    for(;;) {\n          |      String name = String.format(\"msg\");\n          |    }\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |class Example {\n          |  fun log() {\n          |    while(true) {\n          |      val name = String.format(\"msg\")\n          |    }\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun validStringFormatInConstructorCall() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |public class Example {\n          |  public void log() {\n          |    new Exception(String.format(\"msg\"));\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |class Example {\n          |  fun log() {\n          |    Exception(String.format(\"msg\"))\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun validStringFormatInStaticArray() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |public class Example {\n          |  static String[] X = { String.format(\"%s\", 100) };\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |class Example {\n          |  companion object {\n          |    val X = arrayOf(String.format(\"%s\", 100))\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun validStringFormatExtracted() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |    String message = String.format(\"%s\", \"foo\");\n          |    Timber.d(message);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |    val message = String.format(\"%s\", \"foo\")\n          |    Timber.d(message)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun throwableNotAtBeginning() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(\"%s\", e);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(\"%s\", e)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.WHITESPACE)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Throwable should be first argument [ThrowableNotAtBeginning]\n        |     Timber.d(\"%s\", e);\n        |     ~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:6: Warning: Throwable should be first argument [ThrowableNotAtBeginning]\n        |     Timber.d(\"%s\", e)\n        |     ~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 6: Replace with e, \"%s\":\n        |@@ -6 +6\n        |-      Timber.d(\"%s\", e);\n        |+      Timber.d(e, \"%s\");\n        |Fix for src/foo/Example.kt line 6: Replace with e, \"%s\":\n        |@@ -6 +6\n        |-      Timber.d(\"%s\", e)\n        |+      Timber.d(e, \"%s\")\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun stringConcatenationBothLiterals() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(\"foo\" + \"bar\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.d(\"foo\" + \"bar\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun stringConcatenationLeftLiteral() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     String foo = \"foo\";\n          |     Timber.d(foo + \"bar\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val foo = \"foo\"\n          |     Timber.d(\"${\"$\"}{foo}bar\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.PARENTHESIZED)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber]\n        |     Timber.d(foo + \"bar\");\n        |              ~~~~~~~~~~~\n        |0 errors, 1 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 5: Replace with \"%sbar\", foo:\n        |@@ -6 +6\n        |-      Timber.d(foo + \"bar\");\n        |+      Timber.d(\"%sbar\", foo);\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun stringConcatenationRightLiteral() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     String bar = \"bar\";\n          |     Timber.d(\"foo\" + bar);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val bar = \"bar\"\n          |     Timber.d(\"foo${\"$\"}bar\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.PARENTHESIZED)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber]\n        |     Timber.d(\"foo\" + bar);\n        |              ~~~~~~~~~~~\n        |0 errors, 1 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 5: Replace with \"foo%s\", bar:\n        |@@ -6 +6\n        |-      Timber.d(\"foo\" + bar);\n        |+      Timber.d(\"foo%s\", bar);\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun stringConcatenationBothVariables() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     String foo = \"foo\";\n          |     String bar = \"bar\";\n          |     Timber.d(foo + bar);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val foo = \"foo\"\n          |     val bar = \"bar\"\n          |     Timber.d(\"${\"$\"}{foo}${\"$\"}bar\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.PARENTHESIZED)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:7: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber]\n        |     Timber.d(foo + bar);\n        |              ~~~~~~~~~\n        |0 errors, 1 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 6: Replace with \"%s%s\", foo, bar:\n        |@@ -7 +7\n        |-      Timber.d(foo + bar);\n        |+      Timber.d(\"%s%s\", foo, bar);\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun stringConcatenationInsideTernary() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     String s = \"world!\";\n          |     Timber.d(true ? \"Hello, \" + s : \"Bye\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val s = \"world!\"\n          |     Timber.d(if(true) \"Hello, ${\"$\"}{s}\" else \"Bye\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.PARENTHESIZED)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber]\n        |     Timber.d(true ? \"Hello, \" + s : \"Bye\");\n        |                     ~~~~~~~~~~~~~\n        |0 errors, 1 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun tooManyFormatArgs() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(\"%s %s\", \"arg1\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.d(\"%s %s\", \"arg1\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount]\n        |     Timber.d(\"%s %s\", \"arg1\");\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount]\n        |     Timber.d(\"%s %s\", \"arg1\")\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~\n        |2 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun tooManyArgs() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(\"%s\", \"arg1\", \"arg2\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.d(\"%s\", \"arg1\", \"arg2\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount]\n        |     Timber.d(\"%s\", \"arg1\", \"arg2\");\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount]\n        |     Timber.d(\"%s\", \"arg1\", \"arg2\")\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |2 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun wrongArgTypes() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(\"%d\", \"arg1\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.d(\"%d\", \"arg1\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |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]\n        |     Timber.d(\"%d\", \"arg1\");\n        |                    ~~~~~~\n        |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]\n        |     Timber.d(\"%d\", \"arg1\")\n        |                    ~~~~~~\n        |2 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun tagTooLongLiteralOnly() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.tag(\"abcdefghijklmnopqrstuvwx\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        manifest().minSdk(25),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Error: The logging tag can be at most 23 characters, was 24 (abcdefghijklmnopqrstuvwx) [TimberTagLength]\n        |     Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n        |                ~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |1 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun tagTooLongLiteralOnlyBeforeApi26() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.tag(\"abcdefghijklmnopqrstuvwx\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        manifest().minSdk(26),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun tooManyFormatArgsInTag() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.tag(\"tag\").d(\"%s %s\", \"arg1\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.tag(\"tag\").d(\"%s %s\", \"arg1\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount]\n        |     Timber.tag(\"tag\").d(\"%s %s\", \"arg1\");\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Error: Wrong argument count, format string %s %s requires 2 but format call supplies 1 [TimberArgCount]\n        |     Timber.tag(\"tag\").d(\"%s %s\", \"arg1\")\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |2 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun tooManyArgsInTag() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.tag(\"tag\").d(\"%s\", \"arg1\", \"arg2\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.tag(\"tag\").d(\"%s\", \"arg1\", \"arg2\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount]\n        |     Timber.tag(\"tag\").d(\"%s\", \"arg1\", \"arg2\");\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:5: Error: Wrong argument count, format string %s requires 1 but format call supplies 2 [TimberArgCount]\n        |     Timber.tag(\"tag\").d(\"%s\", \"arg1\", \"arg2\")\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |2 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun wrongArgTypesInTag() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.tag(\"tag\").d(\"%d\", \"arg1\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     Timber.tag(\"tag\").d(\"%d\", \"arg1\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |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]\n        |     Timber.tag(\"tag\").d(\"%d\", \"arg1\");\n        |                               ~~~~~~\n        |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]\n        |     Timber.tag(\"tag\").d(\"%d\", \"arg1\")\n        |                               ~~~~~~\n        |2 errors, 0 warnings\n        \"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun exceptionLoggingUsingExceptionMessage() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e.getMessage());\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e.message)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging]\n        |     Timber.d(e.getMessage());\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging]\n        |     Timber.d(e.message)\n        |     ~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 6: Replace message with throwable:\n        |@@ -6 +6\n        |-      Timber.d(e.getMessage());\n        |+      Timber.d(e);\n        |Fix for src/foo/Example.kt line 6: Replace message with throwable:\n        |@@ -6 +6\n        |-      Timber.d(e.message)\n        |+      Timber.d(e)\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun exceptionLoggingUsingExceptionMessageArgument() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e, e.getMessage());\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e, e.message)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS, TestMode.WHITESPACE)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging]\n        |     Timber.d(e, e.getMessage());\n        |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:6: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging]\n        |     Timber.d(e, e.message)\n        |     ~~~~~~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 5: Remove redundant argument:\n        |@@ -6 +6\n        |-      Timber.d(e, e.getMessage());\n        |+      Timber.d(e);\n        |Fix for src/foo/Example.kt line 5: Remove redundant argument:\n        |@@ -6 +6\n        |-      Timber.d(e, e.message)\n        |+      Timber.d(e)\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun exceptionLoggingUsingVariable() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     String msg = \"Hello\";\n          |     Exception e = new Exception();\n          |     Timber.d(e, msg);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val msg = \"Hello\"\n          |     val e = Exception()\n          |     Timber.d(e, msg)  \n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun exceptionLoggingUsingParameter() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log(Exception e, String message) {\n          |     Timber.d(e, message);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log(e: Exception, message: String) {\n          |     Timber.d(e, message)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun exceptionLoggingUsingMethod() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log(Exception e) {\n          |    Timber.d(e, method());\n          |  }\n          |  private String method() {\n          |    return \"foo\";\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log(e: Exception) {\n          |     Timber.d(e, method())\n          |  }\n          |  private fun method(): String {\n          |     return \"foo\"\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun exceptionLoggingUsingNonFinalField() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  private String message;\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e, message);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  private var message = \"\"\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e, message)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun exceptionLoggingUsingFinalField() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  private final String message = \"foo\";\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e, message);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  private val message = \"\"\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e, message)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun exceptionLoggingUsingEmptyStringMessage() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e, \"\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e, \"\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS, TestMode.WHITESPACE)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging]\n        |     Timber.d(e, \"\");\n        |     ~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging]\n        |     Timber.d(e, \"\")\n        |     ~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 6: Remove redundant argument:\n        |@@ -6 +6\n        |-      Timber.d(e, \"\");\n        |+      Timber.d(e);\n        |Fix for src/foo/Example.kt line 6: Remove redundant argument:\n        |@@ -6 +6\n        |-      Timber.d(e, \"\")\n        |+      Timber.d(e)\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun exceptionLoggingUsingNullMessage() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e, null);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e, null)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS, TestMode.WHITESPACE)\n      .run()\n      .expect(\n        \"\"\"\n        |src/foo/Example.java:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging]\n        |     Timber.d(e, null);\n        |     ~~~~~~~~~~~~~~~~~\n        |src/foo/Example.kt:6: Warning: Use single-argument log method instead of null/empty message [TimberExceptionLogging]\n        |     Timber.d(e, null)\n        |     ~~~~~~~~~~~~~~~~~\n        |0 errors, 2 warnings\n        \"\"\"\n          .trimMargin()\n      )\n      .expectFixDiffs(\n        \"\"\"\n        |Fix for src/foo/Example.java line 6: Remove redundant argument:\n        |@@ -6 +6\n        |-      Timber.d(e, null);\n        |+      Timber.d(e);\n        |Fix for src/foo/Example.kt line 6: Remove redundant argument:\n        |@@ -6 +6\n        |-      Timber.d(e, null)\n        |+      Timber.d(e)\n        |\"\"\"\n          .trimMargin()\n      )\n  }\n\n  @Test\n  fun exceptionLoggingUsingValidMessage() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Exception e = new Exception();\n          |     Timber.d(e, \"Valid message\");\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |     val e = Exception()\n          |     Timber.d(e, \"Valid message\")\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .skipTestModes(TestMode.REORDER_ARGUMENTS)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun dateFormatNotDisplayingWarning() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |    Timber.d(\"%tc\", new java.util.Date());\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |    Timber.d(\"%tc\", java.util.Date())\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun systemTimeMillisValidMessage() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |    Timber.d(\"%tc\", System.currentTimeMillis());\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  fun log() {\n          |    Timber.d(\"%tc\", System.currentTimeMillis())\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun wrappedBooleanType() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public void log() {\n          |     Timber.d(\"%b\", Boolean.valueOf(true));\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        // no kotlin equivalent, since primitive wrappers do not exist\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun memberVariable() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        java(\n          \"\"\"\n          |package foo;\n          |import timber.log.Timber;\n          |public class Example {\n          |  public static class Bar {\n          |    public static String baz = \"timber\";\n          |  }\n          |  public void log() {\n          |    Bar bar = new Bar();\n          |    Timber.d(bar.baz);\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |class Example {\n          |  class Bar {\n          |    val baz = \"timber\"\n          |  }\n          |  fun log() {\n          |    val bar = Bar()\n          |    Timber.d(bar.baz)\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .allowClassNameClashes(true)\n      .issues(*issues)\n      .run()\n      .expectClean()\n  }\n\n  @Test\n  fun exceptionLoggingWithGuardedVariable() {\n    lint()\n      .files(\n        TIMBER_STUB,\n        kotlin(\n          \"\"\"\n          |package foo\n          |import timber.log.Timber\n          |import java.lang.Exception\n          |\n          |// This is a fake class just for our test.\n          |interface ApiResponse {\n          |  fun getErrorMessage(): String\n          |}\n          |\n          |class Example {\n          |  fun log(apiResponse: ApiResponse) {\n          |     // Get a message from a method, so the value isn't known at compile time.\n          |     val errorMessage = apiResponse.getErrorMessage()\n          |\n          |     // IMPORTANT: The check that proves the string is not empty.\n          |     if (errorMessage.isNotEmpty()) {\n          |       // This line should NOT cause a lint warning.\n          |       Timber.d(Exception(\"API Error\"), errorMessage)\n          |     }\n          |  }\n          |}\n          \"\"\"\n            .trimMargin()\n        ),\n      )\n      .issues(WrongTimberUsageDetector.ISSUE_EXCEPTION_LOGGING)\n      .run()\n      // We expect this code to be perfectly clean with NO warnings.\n      .expectClean()\n  }\n}\n"
  },
  {
    "path": "timber-sample/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n  namespace 'com.example.timber'\n  compileSdkVersion libs.versions.compileSdk.get().toInteger()\n\n  buildFeatures {\n    buildConfig = true\n    viewBinding = true\n  }\n\n  defaultConfig {\n    applicationId 'com.example.timber'\n    minSdkVersion libs.versions.minSdk.get()\n    targetSdkVersion libs.versions.compileSdk.get().toInteger()\n    versionCode 1\n    versionName '1.0.0'\n  }\n\n  lintOptions {\n    textReport true\n    textOutput 'stdout'\n    ignore 'InvalidPackage'\n  }\n}\n\ndependencies {\n  implementation project(':timber')\n}\n"
  },
  {
    "path": "timber-sample/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n  <application\n      android:allowBackup=\"false\"\n      android:label=\"@string/app_name\"\n      android:name=\".ExampleApp\"\n      tools:ignore=\"GoogleAppIndexingWarning,MissingApplicationIcon\">\n    <activity\n        android:name=\".ui.DemoActivity\"\n        android:exported=\"true\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n      </intent-filter>\n    </activity>\n  </application>\n</manifest>\n"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ExampleApp.java",
    "content": "package com.example.timber;\n\nimport static timber.log.Timber.DebugTree;\n\nimport android.app.Application;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport timber.log.Timber;\n\npublic class ExampleApp extends Application {\n  @Override public void onCreate() {\n    super.onCreate();\n\n    if (BuildConfig.DEBUG) {\n      Timber.plant(new DebugTree());\n    } else {\n      Timber.plant(new CrashReportingTree());\n    }\n  }\n\n  /** A tree which logs important information for crash reporting. */\n  private static class CrashReportingTree extends Timber.Tree {\n    @Override protected void log(int priority, String tag, @NonNull String message, Throwable t) {\n      if (priority == Log.VERBOSE || priority == Log.DEBUG) {\n        return;\n      }\n\n      FakeCrashLibrary.log(priority, tag, message);\n\n      if (t != null) {\n        if (priority == Log.ERROR) {\n          FakeCrashLibrary.logError(t);\n        } else if (priority == Log.WARN) {\n          FakeCrashLibrary.logWarning(t);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/FakeCrashLibrary.java",
    "content": "package com.example.timber;\n\n/** Not a real crash reporting library! */\npublic final class FakeCrashLibrary {\n  public static void log(int priority, String tag, String message) {\n    // TODO add log entry to circular buffer.\n  }\n\n  public static void logWarning(Throwable t) {\n    // TODO report non-fatal warning.\n  }\n\n  public static void logError(Throwable t) {\n    // TODO report non-fatal error.\n  }\n\n  private FakeCrashLibrary() {\n    throw new AssertionError(\"No instances.\");\n  }\n}\n"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ui/DemoActivity.java",
    "content": "package com.example.timber.ui;\n\nimport static android.widget.Toast.LENGTH_SHORT;\n\nimport android.app.Activity;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.Toast;\n\nimport com.example.timber.databinding.DemoActivityBinding;\n\nimport timber.log.Timber;\n\npublic class DemoActivity extends Activity implements View.OnClickListener {\n  @Override protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    DemoActivityBinding binding = DemoActivityBinding.inflate(getLayoutInflater());\n    setContentView(binding.getRoot());\n\n    Timber.tag(\"LifeCycles\");\n    Timber.d(\"Activity Created\");\n\n    binding.hello.setOnClickListener(this);\n    binding.hey.setOnClickListener(this);\n    binding.hi.setOnClickListener(this);\n  }\n\n  @Override public void onClick(View v) {\n    Button button = (Button) v;\n    Timber.i(\"A button with ID %s was clicked to say '%s'.\", button.getId(), button.getText());\n    Toast.makeText(this, \"Check logcat for a greeting!\", LENGTH_SHORT).show();\n  }\n}\n"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ui/JavaLintActivity.java",
    "content": "package com.example.timber.ui;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.os.Bundle;\nimport android.util.Log;\nimport androidx.annotation.Nullable;\nimport timber.log.Timber;\n\nimport static java.lang.String.format;\n\n@SuppressLint(\"Registered\") //\npublic class JavaLintActivity extends Activity {\n  /**\n   * Below are some examples of how NOT to use Timber.\n   *\n   * To see how a particular lint issue behaves, comment/remove its corresponding id from the set\n   * of SuppressLint ids below.\n   */\n  @SuppressLint({\n      \"LogNotTimber\", //\n      \"StringFormatInTimber\", //\n      \"ThrowableNotAtBeginning\", //\n      \"BinaryOperationInTimber\", //\n      \"TimberArgCount\", //\n      \"TimberArgTypes\", //\n      \"TimberTagLength\", //\n      \"TimberExceptionLogging\" //\n  }) //\n  @Override protected void onCreate(@Nullable Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n\n    // LogNotTimber\n    Log.d(\"TAG\", \"msg\");\n    Log.d(\"TAG\", \"msg\", new Exception());\n    android.util.Log.d(\"TAG\", \"msg\");\n    android.util.Log.d(\"TAG\", \"msg\", new Exception());\n\n    // StringFormatInTimber\n    Timber.w(String.format(\"%s\", getString()));\n    Timber.w(format(\"%s\", getString()));\n\n    // ThrowableNotAtBeginning\n    Timber.d(\"%s\", new Exception());\n\n    // BinaryOperationInTimber\n    String foo = \"foo\";\n    String bar = \"bar\";\n    Timber.d(\"foo\" + \"bar\");\n    Timber.d(\"foo\" + bar);\n    Timber.d(foo + \"bar\");\n    Timber.d(foo + bar);\n\n    // TimberArgCount\n    Timber.d(\"%s %s\", \"arg0\");\n    Timber.d(\"%s\", \"arg0\", \"arg1\");\n    Timber.tag(\"tag\").d(\"%s %s\", \"arg0\");\n    Timber.tag(\"tag\").d(\"%s\", \"arg0\", \"arg1\");\n\n    // TimberArgTypes\n    Timber.d(\"%d\", \"arg0\");\n    Timber.tag(\"tag\").d(\"%d\", \"arg0\");\n\n    // TimberTagLength\n    Timber.tag(\"abcdefghijklmnopqrstuvwx\");\n    Timber.tag(\"abcdefghijklmnopqrstuvw\" + \"x\");\n\n    // TimberExceptionLogging\n    Timber.d(new Exception(), new Exception().getMessage());\n    Timber.d(new Exception(), \"\");\n    Timber.d(new Exception(), null);\n    Timber.d(new Exception().getMessage());\n  }\n\n  private String getString() {\n    return \"foo\";\n  }\n}\n"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ui/KotlinLintActivity.kt",
    "content": "package com.example.timber.ui\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.os.Bundle\nimport android.util.Log\nimport java.lang.Exception\nimport java.lang.String.format\nimport timber.log.Timber\n\n@SuppressLint(\"Registered\")\nclass KotlinLintActivity : Activity() {\n  /**\n   * Below are some examples of how NOT to use Timber.\n   *\n   * To see how a particular lint issue behaves, comment/remove its corresponding id from the set of\n   * SuppressLint ids below.\n   */\n  @SuppressLint(\n    \"LogNotTimber\",\n    \"StringFormatInTimber\",\n    \"ThrowableNotAtBeginning\",\n    \"BinaryOperationInTimber\",\n    \"TimberArgCount\",\n    \"TimberArgTypes\",\n    \"TimberTagLength\",\n    \"TimberExceptionLogging\",\n  )\n  @Suppress(\"RemoveRedundantQualifierName\")\n  override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n\n    // LogNotTimber\n    Log.d(\"TAG\", \"msg\")\n    Log.d(\"TAG\", \"msg\", Exception())\n    android.util.Log.d(\"TAG\", \"msg\")\n    android.util.Log.d(\"TAG\", \"msg\", Exception())\n\n    // StringFormatInTimber\n    Timber.w(String.format(\"%s\", getString()))\n    Timber.w(format(\"%s\", getString()))\n\n    // ThrowableNotAtBeginning\n    Timber.d(\"%s\", Exception())\n\n    // BinaryOperationInTimber\n    val foo = \"foo\"\n    val bar = \"bar\"\n    Timber.d(\"foo\" + \"bar\")\n    Timber.d(\"foo$bar\")\n    Timber.d(\"${foo}bar\")\n    Timber.d(\"$foo$bar\")\n\n    // TimberArgCount\n    Timber.d(\"%s %s\", \"arg0\")\n    Timber.d(\"%s\", \"arg0\", \"arg1\")\n    Timber.tag(\"tag\").d(\"%s %s\", \"arg0\")\n    Timber.tag(\"tag\").d(\"%s\", \"arg0\", \"arg1\")\n\n    // TimberArgTypes\n    Timber.d(\"%d\", \"arg0\")\n    Timber.tag(\"tag\").d(\"%d\", \"arg0\")\n\n    // TimberTagLength\n    Timber.tag(\"abcdefghijklmnopqrstuvwx\")\n    Timber.tag(\"abcdefghijklmnopqrstuvw\" + \"x\")\n\n    // TimberExceptionLogging\n    Timber.d(Exception(), Exception().message)\n    Timber.d(Exception(), \"\")\n    Timber.d(Exception(), null)\n    Timber.d(Exception().message)\n  }\n\n  private fun getString() = \"foo\"\n}\n"
  },
  {
    "path": "timber-sample/src/main/res/layout/demo_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"40dp\">\n  <Button\n      android:id=\"@+id/hello\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_marginBottom=\"10dp\"\n      android:text=\"@string/hello\"\n      />\n  <Button\n      android:id=\"@+id/hi\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:layout_marginBottom=\"10dp\"\n      android:text=\"@string/hi\"\n      />\n  <Button\n      android:id=\"@+id/hey\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:text=\"@string/hey\"\n      />\n</LinearLayout>\n"
  },
  {
    "path": "timber-sample/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources>\n  <string name=\"app_name\">Timber</string>\n\n  <string name=\"hello\">Hello</string>\n  <string name=\"hi\">Hi</string>\n  <string name=\"hey\">Hey</string>\n</resources>\n"
  }
]