Full Code of JakeWharton/timber for AI

trunk 674829a3ff8c cached
41 files
230.4 KB
58.1k tokens
18 symbols
1 requests
Download .txt
Showing preview only (244K chars total). Download the full file or copy to clipboard to get everything.
Repository: JakeWharton/timber
Branch: trunk
Commit: 674829a3ff8c
Files: 41
Total size: 230.4 KB

Directory structure:
gitextract_ztbbr8oj/

├── .editorconfig
├── .github/
│   ├── renovate.json5
│   └── workflows/
│       ├── .java-version
│       ├── build.yaml
│       └── release.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── RELEASING.md
├── build.gradle
├── gradle/
│   ├── libs.versions.toml
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── timber/
│   ├── api/
│   │   └── timber.api
│   ├── build.gradle
│   ├── consumer-keep-rules.pro
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   └── kotlin/
│       │       └── timber/
│       │           └── log/
│       │               └── Timber.kt
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── timber/
│       │           └── log/
│       │               └── Timber.kt
│       └── test/
│           └── java/
│               └── timber/
│                   └── log/
│                       ├── TimberJavaTest.java
│                       └── TimberTest.kt
├── timber-lint/
│   ├── build.gradle
│   ├── gradle.properties
│   ├── lint-baseline.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── timber/
│       │           └── lint/
│       │               ├── TimberIssueRegistry.kt
│       │               └── WrongTimberUsageDetector.kt
│       └── test/
│           └── java/
│               └── timber/
│                   └── lint/
│                       └── WrongTimberUsageDetectorTest.kt
└── timber-sample/
    ├── build.gradle
    └── src/
        └── main/
            ├── AndroidManifest.xml
            ├── java/
            │   └── com/
            │       └── example/
            │           └── timber/
            │               ├── ExampleApp.java
            │               ├── FakeCrashLibrary.java
            │               └── ui/
            │                   ├── DemoActivity.java
            │                   ├── JavaLintActivity.java
            │                   └── KotlinLintActivity.kt
            └── res/
                ├── layout/
                │   └── demo_activity.xml
                └── values/
                    └── strings.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
insert_final_newline=true
end_of_line=lf
charset=utf-8
indent_size=2
trim_trailing_whitespace=true

[*.{kt,kts}]
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ij_kotlin_imports_layout=*


================================================
FILE: .github/renovate.json5
================================================
{
	$schema: 'https://docs.renovatebot.com/renovate-schema.json',
	extends: [
		'config:recommended',
	],
	packageRules: [
		{
			groupName: 'AGP',
			matchPackagePatterns: [
				'com.android.tools.*',
			],
		},
	],
	ignorePresets: [
		// Ensure we get the latest version and are not pinned to old versions.
		'workarounds:javaLTSVersions',
	],
	customManagers: [
		// Update .java-version file with the latest JDK version.
		{
			customType: 'regex',
			fileMatch: [
				'\\.java-version$',
			],
			matchStrings: [
				'(?<currentValue>.*)\\n',
			],
			datasourceTemplate: 'java-version',
			depNameTemplate: 'java',
			// Only write the major version.
			extractVersionTemplate: '^(?<version>\\d+)',
		},
	],
}


================================================
FILE: .github/workflows/.java-version
================================================
25


================================================
FILE: .github/workflows/build.yaml
================================================
name: build

on:
  pull_request: {}
  workflow_dispatch: {}
  push:
    branches:
      - 'trunk'
    tags-ignore:
      - '**'

env:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.vfs.watch=false -Dkotlin.incremental=false  -Dorg.gradle.logging.stacktrace=full"

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-java@v5
        with:
          distribution: 'zulu'
          java-version-file: .github/workflows/.java-version
      - uses: gradle/actions/setup-gradle@v5

      - run: ./gradlew build dokkaGenerate

      - run: ./gradlew publish
        if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/timber' }}
        env:
          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}
          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}
          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }}

      - name: Deploy docs to website
        if: ${{ github.ref == 'refs/heads/trunk' && github.repository == 'JakeWharton/timber' }}
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: site
          FOLDER: timber/build/dokka/html/
          TARGET_FOLDER: docs/latest/
          CLEAN: true


================================================
FILE: .github/workflows/release.yaml
================================================
name: release

on:
  push:
    tags:
      - '**'

env:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.vfs.watch=false -Dkotlin.incremental=false  -Dorg.gradle.logging.stacktrace=full"

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-java@v5
        with:
          distribution: 'zulu'
          java-version-file: .github/workflows/.java-version
      - uses: gradle/actions/setup-gradle@v5

      - run: ./gradlew -p mosaic publish
        env:
          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}
          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}
          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }}

      - name: Extract release notes
        id: release_notes
        uses: ffurrer2/extract-release-notes@v3

      - name: Create release
        uses: softprops/action-gh-release@v2
        with:
          body: ${{ steps.release_notes.outputs.release_notes }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - run: ./gradlew dokkaGenerate

      - name: Deploy docs to website
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: site
          FOLDER: timber/build/dokka/html/
          TARGET_FOLDER: docs/5.x/
          CLEAN: true


================================================
FILE: .gitignore
================================================
.classpath
.project
.settings
eclipsebin

bin
gen
build
out
lib

.idea
*.iml
classes

obj

.DS_Store

# Gradle
.gradle
jniLibs
build
local.properties
reports


================================================
FILE: CHANGELOG.md
================================================
# Change log

## [Unreleased]


## [5.0.1] - 2021-08-13

### Fixed

- Fix TimberArgCount lint check false positive on some calls to `String.format`.


## [5.0.0] - 2021-08-10

The library has been rewritten in Kotlin, but it remains binary-compatible with 4.x.
The intent is to support Kotlin multiplatform in the future.
This is otherwise a relatively minor, bug-fix release.

### Changed

- Minimum supported API level is now 14.
- Minimum supported AGP (for embedded lint checks) is now 7.0.

### Fixed

- `DebugTree` now finds first non-library class name which prevents exceptions in optimized builds where expected stackframes may have been inlined.
- Enforce 23-character truncated tag length until API 26 per AOSP sources.
- Support `Long` type for date/time format arguments when validating format strings in lint checks.
- Do not report string literal concatenation in lint checks on log message.


## [4.7.1] - 2018-06-28

 * Fix: Redundant argument lint check now works correctly on Kotlin sources.


## [4.7.0] - 2018-03-27

 * Fix: Support lint version 26.1.0.
 * Fix: Check single-argument log method in TimberExceptionLogging.


## [4.6.1] - 2018-02-12

 * Fix: Lint checks now handle more edge cases around exception and message source.
 * Fix: Useless `BuildConfig` class is no longer included.


## [4.6.0] - 2017-10-30

 * 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.
 * New: Added nullability annotations for Kotlin users.
 * Fix: Tag length truncation no longer occurs on API 24 or newer as the system no longer has a length restriction.
 * Fix: Handle when a `null` array is supplied for the message arguments. This can occur when using various bytecode optimization tools.


## [4.5.1] - 2017-01-20

 * Fix: String formatting lint check now correctly works with dates.


## [4.5.0] - 2017-01-09

 * New: Automatically truncate class name tags to Android's limit of 23 characters.
 * New: Lint check for detecting null/empty messages or using the exception message when logging an
   exception. Use the single-argument logging overloads instead.
 * Fix: Correct NPE in lint check when using String.format.


## [4.4.0] - 2016-12-06

 * New: `Tree.formatMessage` method allows customization of message formatting and rendering.
 * New: Lint checks ported to new IntelliJ PSI infrastructure.


## [4.3.1] - 2016-09-19

 * New: Add `isLoggable` convenience method which also provides the tag.


## [4.3.0] - 2016-08-18

 * New: Overloads for all log methods which accept only a `Throwable` without a message.


## [4.2.0] - 2016-08-12

 * New: `Timber.plant` now has a varargs overload for planting multiple trees at once.
 * New: minSdkVersion is now 9 because reasons.
 * Fix: Consume explicitly specified tag even if the message is determined as not loggable (due to level).
 * Fix: Allow lint checks to run when `Timber.tag(..).v(..)`-style logging is used.


## [4.1.2] - 2016-03-30

 * Fix: Tag-length lint check now only triggers on calls to `Timber`'s `tag` method. Previously it would
   match _any_ `tag` method and flag arguments longer than 23 characters.


## [4.1.1] - 2016-02-19

 * New: Add method for retreiving the number of installed trees.


## [4.1.0] - 2015-10-19

 * New: Consumer ProGuard rule automatically suppresses a warning for the use `@NonNls` on the 'message'
   argument for logging method. The warning was only for users running ProGuard and can safely be ignored.
 * New: Static `log` methods which accept a priority as a first argument makes dynamic logging at different
   levels easier to support.
 * Fix: Replace internal use of `Log.getStackTraceString` with our own implementation. This ensures that
   `UnknownHostException` errors are logged, which previously were suppressed.
 * Fix: 'BinaryOperationInTimber' lint rule now only triggers for string concatenation.


## [4.0.1] - 2015-10-07

 * Fix: TimberArgTypes lint rule now allows booleans and numbers in '%s' format markers.
 * Fix: Lint rules now support running on Java 7 VMs.


## [4.0.0] - 2015-10-07

 * New: Library is now an .aar! This means the lint rules are automatically applied to consuming
   projects.
 * New: `Tree.forest()` returns an immutable copy of all planted trees.
 * Fix: Ensure thread safety when logging and adding or removing trees concurrently.


## [3.1.0] - 2015-05-11

 * New: `Tree.isLoggable` method allows a tree to determine whether a statement should be logged
   based on its priority. Defaults to logging all levels.


## [3.0.2] - 2015-05-01

 * Fix: Strip multiple anonymous class markers (e.g., `$1$2`) from class names when `DebugTree`
   is creating an inferred tag.


## [3.0.1] - 2015-04-17

 * Fix: String formatting is now always applied when arguments are present. Previously it would
   only trigger when an exception was included.


## [3.0.0] - 2015-04-16

 * New: `Tree` and `DebugTree` APIs are much more extensible requiring only a single method to
   override.
 * New: `DebugTree` exposes `createStackElementTag` method for overriding to customize the
   reflection-based tag creation (for example, such as to add a line number).
 * WTF: Support for `wtf` log level.
 * `HollowTree` has been removed as it is no longer needed. Just extend `Tree`.
 * `TaggedTree` has been removed and its functionality folded into `Tree`. All `Tree` instances
   will receive any tags specified by a call to `tag`.
 * Fix: Multiple planted `DebugTree`s now each correctly received tags set from a call to `tag`.


## [2.7.1] - 2015-02-17

 * Fix: Switch method of getting calling class to be consistent across API levels.


## [2.7.0] - 2015-02-17

 * New: `DebugTree` subclasses can now override `logMessage` for access to the priority, tag, and
   entire message for every log.
 * Fix: Prevent overriding `Tree` and `TaggedTree` methods on `DebugTree`.


## [2.6.0] - 2015-02-17

 * New: `DebugTree` subclasses can now override `createTag()` to specify log tags. `nextTag()` is
   also accessible for querying if an explicit tag was set.


## [2.5.1] - 2015-01-19

 * Fix: Properly split lines which contain both newlines and are over 4000 characters.
 * Explicitly forbid `null` tree instances.


## [2.5.0] - 2014-11-08

 * New: `Timber.asTree()` exposes functionality as a `Tree` instance rather than static methods.


## [2.4.2] - 2014-11-07

 * Eliminate heap allocation when dispatching log calls.


## [2.4.1] - 2014-06-19

 * Fix: Calls with no message but a `Throwable` are now correctly logged.


## [2.4.0] - 2014-06-10

 * New: `uproot` and `uprootAll` methods allow removing trees.


## [2.3.0] - 2014-05-21

 * New: Messages longer than 4000 characters will be split into multiple lines.


## [2.2.2] - 2014-02-12

 * Fix: Include debug level in previous fix which avoids formatting messages with no arguments.


## [2.2.1] - 2014-02-11

 * Fix: Do not attempt to format log messages which do not have arguments.


## [2.2.0] - 2014-02-02

 * New: verbose log level added (`v()`).
 * New: `timber-lint` module adds lint check to ensure you are calling `Timber` and not `Log`.
 * Fix: Specifying custom tags is now thread-safe.


## [2.1.0] - 2013-11-21

 * New: `tag` method allows specifying custom one-time tag. Redux!


## [2.0.0] - 2013-10-21

 * Logging API is now exposed as static methods on `Timber`. Behavior is added by installing `Tree`
   instances for logging.


## [1.1.0] - 2013-07-22

 * New: `tag` method allows specifying custom one-time tag.
 * Fix: Exception-containing methods now log at the correct level.


## [1.0.0] - 2013-07-17

Initial cut. (Get it?)




[Unreleased]: https://github.com/JakeWharton/timber/compare/5.0.1...HEAD
[5.0.1]: https://github.com/JakeWharton/timber/releases/tag/5.0.1
[5.0.0]: https://github.com/JakeWharton/timber/releases/tag/5.0.0
[4.7.1]: https://github.com/JakeWharton/timber/releases/tag/4.7.1
[4.7.0]: https://github.com/JakeWharton/timber/releases/tag/4.7.0
[4.6.1]: https://github.com/JakeWharton/timber/releases/tag/4.6.1
[4.6.0]: https://github.com/JakeWharton/timber/releases/tag/4.6.0
[4.5.1]: https://github.com/JakeWharton/timber/releases/tag/4.5.1
[4.5.0]: https://github.com/JakeWharton/timber/releases/tag/4.5.0
[4.4.0]: https://github.com/JakeWharton/timber/releases/tag/4.4.0
[4.3.1]: https://github.com/JakeWharton/timber/releases/tag/4.3.1
[4.3.0]: https://github.com/JakeWharton/timber/releases/tag/4.3.0
[4.2.0]: https://github.com/JakeWharton/timber/releases/tag/4.2.0
[4.1.2]: https://github.com/JakeWharton/timber/releases/tag/4.1.2
[4.1.1]: https://github.com/JakeWharton/timber/releases/tag/4.1.1
[4.1.0]: https://github.com/JakeWharton/timber/releases/tag/4.1.0
[4.0.1]: https://github.com/JakeWharton/timber/releases/tag/4.0.1
[4.0.0]: https://github.com/JakeWharton/timber/releases/tag/4.0.0
[3.1.0]: https://github.com/JakeWharton/timber/releases/tag/3.1.0
[3.0.2]: https://github.com/JakeWharton/timber/releases/tag/3.0.2
[3.0.1]: https://github.com/JakeWharton/timber/releases/tag/3.0.1
[3.0.0]: https://github.com/JakeWharton/timber/releases/tag/3.0.0
[2.7.1]: https://github.com/JakeWharton/timber/releases/tag/2.7.1
[2.7.0]: https://github.com/JakeWharton/timber/releases/tag/2.7.0
[2.6.0]: https://github.com/JakeWharton/timber/releases/tag/2.6.0
[2.5.1]: https://github.com/JakeWharton/timber/releases/tag/2.5.1
[2.5.0]: https://github.com/JakeWharton/timber/releases/tag/2.5.0
[2.4.2]: https://github.com/JakeWharton/timber/releases/tag/2.4.2
[2.4.1]: https://github.com/JakeWharton/timber/releases/tag/2.4.1
[2.4.0]: https://github.com/JakeWharton/timber/releases/tag/2.4.0
[2.3.0]: https://github.com/JakeWharton/timber/releases/tag/2.3.0
[2.2.2]: https://github.com/JakeWharton/timber/releases/tag/2.2.2
[2.2.1]: https://github.com/JakeWharton/timber/releases/tag/2.2.1
[2.2.0]: https://github.com/JakeWharton/timber/releases/tag/2.2.0
[2.1.0]: https://github.com/JakeWharton/timber/releases/tag/2.1.0
[2.0.0]: https://github.com/JakeWharton/timber/releases/tag/2.0.0
[1.1.0]: https://github.com/JakeWharton/timber/releases/tag/1.1.0
[1.0.0]: https://github.com/JakeWharton/timber/releases/tag/1.0.0


================================================
FILE: LICENSE.txt
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
![Timber](logo.png)

This is a logger with a small, extensible API which provides utility on top of Android's normal
`Log` class.

I copy this class into all the little apps I make. I'm tired of doing it. Now it's a library.

Behavior is added through `Tree` instances. You can install an instance by calling `Timber.plant`.
Installation of `Tree`s should be done as early as possible. The `onCreate` of your application is
the most logical choice.

The `DebugTree` implementation will automatically figure out from which class it's being called and
use that class name as its tag. Since the tags vary, it works really well when coupled with a log
reader like [Pidcat][1].

There are no `Tree` implementations installed by default because every time you log in production, a
puppy dies.


Usage
-----

Two easy steps:

 1. Install any `Tree` instances you want in the `onCreate` of your application class.
 2. Call `Timber`'s static methods everywhere throughout your app.

Check out the sample app in `timber-sample/` to see it in action.


Lint
----

Timber ships with embedded lint rules to detect problems in your app.

 *  **TimberArgCount** (Error) - Detects an incorrect number of arguments passed to a `Timber` call for
    the specified format string.

        Example.java:35: Error: Wrong argument count, format string Hello %s %s! requires 2 but format call supplies 1 [TimberArgCount]
            Timber.d("Hello %s %s!", firstName);
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 *  **TimberArgTypes** (Error) - Detects arguments which are of the wrong type for the specified format string.

        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]
            Timber.d("success = %b", taskName);
                                     ~~~~~~~~
 *  **TimberTagLength** (Error) - Detects the use of tags which are longer than Android's maximum length of 23.

        Example.java:35: Error: The logging tag can be at most 23 characters, was 35 (TagNameThatIsReallyReallyReallyLong) [TimberTagLength]
            Timber.tag("TagNameThatIsReallyReallyReallyLong").d("Hello %s %s!", firstName, lastName);
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 *  **LogNotTimber** (Warning) - Detects usages of Android's `Log` that should be using `Timber`.

        Example.java:35: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
            Log.d("Greeting", "Hello " + firstName + " " + lastName + "!");
                ~

 *  **StringFormatInTimber** (Warning) - Detects `String.format` used inside of a `Timber` call. Timber
    handles string formatting automatically.

        Example.java:35: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
            Timber.d(String.format("Hello, %s %s", firstName, lastName));
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 *  **BinaryOperationInTimber** (Warning) - Detects string concatenation inside of a `Timber` call. Timber
    handles string formatting automatically and should be preferred over manual concatenation.

        Example.java:35: Warning: Replace String concatenation with Timber's string formatting [BinaryOperationInTimber]
            Timber.d("Hello " + firstName + " " + lastName + "!");
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 *  **TimberExceptionLogging** (Warning) - Detects the use of null or empty messages, or using the exception message
    when logging an exception.

        Example.java:35: Warning: Explicitly logging exception message is redundant [TimberExceptionLogging]
             Timber.d(e, e.getMessage());
                         ~~~~~~~~~~~~~~


Download
--------

```groovy
repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.jakewharton.timber:timber:5.0.1'
}
```

Documentation is available at [jakewharton.github.io/timber/docs/5.x/](https://jakewharton.github.io/timber/docs/5.x/).

<details>
<summary>Snapshots of the development version are available in Sonatype's snapshots repository.</summary>
<p>

```groovy
repositories {
  mavenCentral()
  maven {
    url 'https://central.sonatype.com/repository/maven-snapshots/'
  }
}

dependencies {
  implementation 'com.jakewharton.timber:timber:5.1.0-SNAPSHOT'
}
```

Snapshot documentation is available at [jakewharton.github.io/timber/docs/latest/](https://jakewharton.github.io/timber/docs/latest/).

</p>
</details>


License
-------

    Copyright 2013 Jake Wharton

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.



 [1]: http://github.com/JakeWharton/pidcat/
 [snap]: https://oss.sonatype.org/content/repositories/snapshots/


================================================
FILE: RELEASING.md
================================================
# Releasing

1. Update the `VERSION_NAME` in `gradle.properties` to the release version.

2. Update the `CHANGELOG.md`:
   1. Change the `Unreleased` header to the release version.
   2. Add a link URL to ensure the header link works.
   3. Add a new `Unreleased` section to the top.

3. Update the `README.md`:
   1. Change the "Download" section to reflect the new release version.
   2. Change the snapshot section to reflect the next "SNAPSHOT" version, if it is changing.
   3. Update the Kotlin version compatibility table

4. Commit

   ```
   $ git commit -am "Prepare version X.Y.X"
   ```

5. Manually release and upload artifacts
   1. Run `./gradlew clean publish`
   2. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.
   3. If either fails, drop the Sonatype repo, fix the problem, commit, and restart this section.

6. Tag

   ```
   $ git tag -am "Version X.Y.Z" X.Y.Z
   ```

7. Update the `VERSION_NAME` in `gradle.properties` to the next "SNAPSHOT" version.

8. Commit

   ```
   $ git commit -am "Prepare next development version"
   ```

9. Push!

   ```
   $ git push && git push --tags
   ```

   This will trigger a GitHub Action workflow which will create a GitHub release.


================================================
FILE: build.gradle
================================================
buildscript {
  repositories {
    mavenCentral()
    google()
    gradlePluginPortal()
  }

  dependencies {
    classpath libs.gradlePlugin.android
    classpath libs.gradlePlugin.kotlin
    classpath libs.gradlePlugin.japicmp
    classpath libs.gradlePlugin.mavenPublish
    classpath libs.gradlePlugin.dokka
    classpath libs.compatPlugin
    classpath libs.spotlessPlugin
  }
}

subprojects {
  repositories {
    mavenCentral()
    google()
  }

  apply plugin: 'com.diffplug.spotless'
  spotless {
    kotlin {
      ktfmt(libs.ktfmt.get().version).googleStyle()
      target('**/*.kt')
    }
  }

  tasks.withType(Test) {
    testLogging {
      events "failed"
      exceptionFormat "full"
      showExceptions true
      showStackTraces true
      showCauses true
    }
  }
}


================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
agp = "9.1.0"
androidTools = "32.1.0" # Update this values in sync with agp.
kotlin = "2.3.20"
autoService = "1.1.1"
minSdk = "14"
compileSdk = "36"

[libraries]
gradlePlugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" }
gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
gradlePlugin-dokka = "org.jetbrains.dokka:dokka-gradle-plugin:2.1.0"
gradlePlugin-japicmp = "me.champeau.gradle:japicmp-gradle-plugin:0.4.6"
gradlePlugin-mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.36.0"
compatPlugin = "com.gradleup.tapmoc:tapmoc-gradle-plugin:0.4.0"

annotations = "org.jetbrains:annotations:26.1.0"
auto-service = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
auto-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }

lint-core = { module = "com.android.tools.lint:lint", version.ref = "androidTools" }
lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "androidTools" }
lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "androidTools" }
lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "androidTools" }

junit = "junit:junit:4.13.2"
assertk = "com.willowtreeapps.assertk:assertk:0.28.1"
robolectric = "org.robolectric:robolectric:4.16.1"

spotlessPlugin = "com.diffplug.spotless:spotless-plugin-gradle:8.4.0"
ktfmt = "com.facebook:ktfmt:0.62"


================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: gradle.properties
================================================
GROUP=com.jakewharton.timber

# HEY! If you change the major version here be sure to update release.yaml doc target folder!
VERSION_NAME=5.1.0-SNAPSHOT

POM_DESCRIPTION=No-nonsense injectable logging.

POM_URL=https://github.com/JakeWharton/timber
POM_SCM_URL=https://github.com/JakeWharton/timber
POM_SCM_CONNECTION=scm:git:git://github.com/JakeWharton/timber.git
POM_SCM_DEV_CONNECTION=scm:git:git://github.com/JakeWharton/timber.git

POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo

POM_DEVELOPER_ID=jakewharton
POM_DEVELOPER_NAME=Jake Wharton

org.gradle.jvmargs=-Xmx1536M

mavenCentralPublishing=true
mavenCentralAutomaticPublishing=true
signAllPublications=true


================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac



# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

:execute
@rem Setup the command line



@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: settings.gradle
================================================
include ':timber'
include ':timber-lint'
include ':timber-sample'

rootProject.name = 'timber-root'


================================================
FILE: timber/api/timber.api
================================================
public final class timber/log/Timber {
	public static final field Forest Ltimber/log/Timber$Forest;
	public static fun asTree ()Ltimber/log/Timber$Tree;
	public static fun d (Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun d (Ljava/lang/Throwable;)V
	public static fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun e (Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun e (Ljava/lang/Throwable;)V
	public static fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public static final fun forest ()Ljava/util/List;
	public static fun i (Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun i (Ljava/lang/Throwable;)V
	public static fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun log (ILjava/lang/String;[Ljava/lang/Object;)V
	public static fun log (ILjava/lang/Throwable;)V
	public static fun log (ILjava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public static final fun plant (Ltimber/log/Timber$Tree;)V
	public static final fun plant ([Ltimber/log/Timber$Tree;)V
	public static final fun tag (Ljava/lang/String;)Ltimber/log/Timber$Tree;
	public static final fun treeCount ()I
	public static final fun uproot (Ltimber/log/Timber$Tree;)V
	public static final fun uprootAll ()V
	public static fun v (Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun v (Ljava/lang/Throwable;)V
	public static fun v (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun w (Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun w (Ljava/lang/Throwable;)V
	public static fun w (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun wtf (Ljava/lang/String;[Ljava/lang/Object;)V
	public static fun wtf (Ljava/lang/Throwable;)V
	public static fun wtf (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}

public class timber/log/Timber$DebugTree : timber/log/Timber$Tree {
	public static final field Companion Ltimber/log/Timber$DebugTree$Companion;
	public fun <init> ()V
	protected fun createStackElementTag (Ljava/lang/StackTraceElement;)Ljava/lang/String;
	protected fun log (ILjava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
}

public final class timber/log/Timber$DebugTree$Companion {
}

public final class timber/log/Timber$Forest : timber/log/Timber$Tree {
	public fun asTree ()Ltimber/log/Timber$Tree;
	public fun d (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun d (Ljava/lang/Throwable;)V
	public fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun e (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun e (Ljava/lang/Throwable;)V
	public fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public final fun forest ()Ljava/util/List;
	public fun i (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun i (Ljava/lang/Throwable;)V
	public fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun log (ILjava/lang/String;[Ljava/lang/Object;)V
	public fun log (ILjava/lang/Throwable;)V
	public fun log (ILjava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public final fun plant (Ltimber/log/Timber$Tree;)V
	public final fun plant ([Ltimber/log/Timber$Tree;)V
	public final fun tag (Ljava/lang/String;)Ltimber/log/Timber$Tree;
	public final fun treeCount ()I
	public final fun uproot (Ltimber/log/Timber$Tree;)V
	public final fun uprootAll ()V
	public fun v (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun v (Ljava/lang/Throwable;)V
	public fun v (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun w (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun w (Ljava/lang/Throwable;)V
	public fun w (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun wtf (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun wtf (Ljava/lang/Throwable;)V
	public fun wtf (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}

public abstract class timber/log/Timber$Tree {
	public fun <init> ()V
	public fun d (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun d (Ljava/lang/Throwable;)V
	public fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun e (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun e (Ljava/lang/Throwable;)V
	public fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	protected fun formatMessage (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
	public fun i (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun i (Ljava/lang/Throwable;)V
	public fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	protected fun isLoggable (I)Z
	protected fun isLoggable (Ljava/lang/String;I)Z
	protected abstract fun log (ILjava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
	public fun log (ILjava/lang/String;[Ljava/lang/Object;)V
	public fun log (ILjava/lang/Throwable;)V
	public fun log (ILjava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun v (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun v (Ljava/lang/Throwable;)V
	public fun v (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun w (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun w (Ljava/lang/Throwable;)V
	public fun w (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
	public fun wtf (Ljava/lang/String;[Ljava/lang/Object;)V
	public fun wtf (Ljava/lang/Throwable;)V
	public fun wtf (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}



================================================
FILE: timber/build.gradle
================================================
import tapmoc.Severity

apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'com.android.kotlin.multiplatform.library'
apply plugin: 'com.vanniktech.maven.publish'
apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.
apply plugin: 'com.gradleup.tapmoc'

kotlin {
  androidLibrary {
    namespace = 'timber.log'
    compileSdk = libs.versions.compileSdk.get().toInteger()
    minSdk = libs.versions.minSdk.get().toInteger()

    optimization {
      // "it" --> https://issuetracker.google.com/issues/445115242
      it.consumerKeepRules.file('consumer-keep-rules.pro')
    }

    lint {
      it.textReport = true
    }
  }

  sourceSets {
    commonMain {
      dependencies {
        implementation libs.annotations
      }
    }
    commonTest {
      dependencies {
        implementation libs.annotations
        implementation libs.junit
        implementation libs.assertk
        implementation libs.robolectric
      }
    }
  }

  abiValidation {
    enabled = true
  }
}

tapmoc {
  java(8)
  kotlin('2.0.0')
  checkDependencies(Severity.ERROR)
}

tasks.named('check') { check ->
  check.dependsOn(
      // TODO: https://youtrack.jetbrains.com/issue/KT-78525
      tasks.named('checkLegacyAbi'),
  )
}

dependencies {
  lintPublish project(':timber-lint')
}


================================================
FILE: timber/consumer-keep-rules.pro
================================================
-dontwarn org.jetbrains.annotations.**


================================================
FILE: timber/gradle.properties
================================================
POM_ARTIFACT_ID=timber
POM_NAME=Timber


================================================
FILE: timber/src/androidMain/kotlin/timber/log/Timber.kt
================================================
package timber.log

import android.os.Build
import android.util.Log
import java.io.PrintWriter
import java.io.StringWriter
import java.util.ArrayList
import java.util.Collections
import java.util.Collections.unmodifiableList
import java.util.regex.Pattern
import org.jetbrains.annotations.NonNls

/** Logging for lazy people. */
actual class Timber actual private constructor() {
  init {
    throw AssertionError()
  }

  /** A facade for handling logging calls. Install instances via [`Timber.plant()`][.plant]. */
  actual abstract class Tree {
    @get:JvmSynthetic // Hide from public API.
    internal val explicitTag = ThreadLocal<String>()

    @get:JvmSynthetic // Hide from public API.
    internal open val tag: String?
      get() {
        val tag = explicitTag.get()
        if (tag != null) {
          explicitTag.remove()
        }
        return tag
      }

    /** Log a verbose message with optional format args. */
    actual open fun v(message: String?, vararg args: Any?) {
      prepareLog(Log.VERBOSE, null, message, *args)
    }

    /** Log a verbose exception and a message with optional format args. */
    actual open fun v(t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(Log.VERBOSE, t, message, *args)
    }

    /** Log a verbose exception. */
    actual open fun v(t: Throwable?) {
      prepareLog(Log.VERBOSE, t, null)
    }

    /** Log a debug message with optional format args. */
    actual open fun d(message: String?, vararg args: Any?) {
      prepareLog(Log.DEBUG, null, message, *args)
    }

    /** Log a debug exception and a message with optional format args. */
    actual open fun d(t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(Log.DEBUG, t, message, *args)
    }

    /** Log a debug exception. */
    actual open fun d(t: Throwable?) {
      prepareLog(Log.DEBUG, t, null)
    }

    /** Log an info message with optional format args. */
    actual open fun i(message: String?, vararg args: Any?) {
      prepareLog(Log.INFO, null, message, *args)
    }

    /** Log an info exception and a message with optional format args. */
    actual open fun i(t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(Log.INFO, t, message, *args)
    }

    /** Log an info exception. */
    actual open fun i(t: Throwable?) {
      prepareLog(Log.INFO, t, null)
    }

    /** Log a warning message with optional format args. */
    actual open fun w(message: String?, vararg args: Any?) {
      prepareLog(Log.WARN, null, message, *args)
    }

    /** Log a warning exception and a message with optional format args. */
    actual open fun w(t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(Log.WARN, t, message, *args)
    }

    /** Log a warning exception. */
    actual open fun w(t: Throwable?) {
      prepareLog(Log.WARN, t, null)
    }

    /** Log an error message with optional format args. */
    actual open fun e(message: String?, vararg args: Any?) {
      prepareLog(Log.ERROR, null, message, *args)
    }

    /** Log an error exception and a message with optional format args. */
    actual open fun e(t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(Log.ERROR, t, message, *args)
    }

    /** Log an error exception. */
    actual open fun e(t: Throwable?) {
      prepareLog(Log.ERROR, t, null)
    }

    /** Log an assert message with optional format args. */
    actual open fun wtf(message: String?, vararg args: Any?) {
      prepareLog(Log.ASSERT, null, message, *args)
    }

    /** Log an assert exception and a message with optional format args. */
    actual open fun wtf(t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(Log.ASSERT, t, message, *args)
    }

    /** Log an assert exception. */
    actual open fun wtf(t: Throwable?) {
      prepareLog(Log.ASSERT, t, null)
    }

    /** Log at `priority` a message with optional format args. */
    actual open fun log(priority: Int, message: String?, vararg args: Any?) {
      prepareLog(priority, null, message, *args)
    }

    /** Log at `priority` an exception and a message with optional format args. */
    actual open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?) {
      prepareLog(priority, t, message, *args)
    }

    /** Log at `priority` an exception. */
    actual open fun log(priority: Int, t: Throwable?) {
      prepareLog(priority, t, null)
    }

    /** Return whether a message at `priority` should be logged. */
    @Deprecated("Use isLoggable(String, int)", ReplaceWith("this.isLoggable(null, priority)"))
    protected open fun isLoggable(priority: Int): Boolean = true

    /** Return whether a message at `priority` or `tag` should be logged. */
    actual protected open fun isLoggable(tag: String?, priority: Int): Boolean =
      isLoggable(priority)

    private fun prepareLog(priority: Int, t: Throwable?, message: String?, vararg args: Any?) {
      // Consume tag even when message is not loggable so that next message is correctly tagged.
      val tag = tag
      if (!isLoggable(tag, priority)) {
        return
      }

      var message = message
      if (message.isNullOrEmpty()) {
        if (t == null) {
          return // Swallow message if it's null and there's no throwable.
        }
        message = getStackTraceString(t)
      } else {
        if (args.isNotEmpty()) {
          message = formatMessage(message, args)
        }
        if (t != null) {
          message += "\n" + getStackTraceString(t)
        }
      }

      log(priority, tag, message, t)
    }

    /** Formats a log message with optional arguments. */
    actual protected open fun formatMessage(message: String, args: Array<out Any?>): String =
      message.format(*args)

    private fun getStackTraceString(t: Throwable): String {
      // Don't replace this with Log.getStackTraceString() - it hides
      // UnknownHostException, which is not what we want.
      val sw = StringWriter(256)
      val pw = PrintWriter(sw, false)
      t.printStackTrace(pw)
      pw.flush()
      return sw.toString()
    }

    /**
     * Write a log message to its destination. Called for all level-specific methods by default.
     *
     * @param priority Log level. See [Log] for constants.
     * @param tag Explicit or inferred tag. May be `null`.
     * @param message Formatted log message.
     * @param t Accompanying exceptions. May be `null`.
     */
    actual protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?)
  }

  /** A [Tree] for debug builds. Automatically infers the tag from the calling class. */
  open class DebugTree : Tree() {
    private val fqcnIgnore =
      listOf(
        Timber::class.java.name,
        Forest::class.java.name,
        Tree::class.java.name,
        DebugTree::class.java.name,
      )

    override val tag: String?
      get() =
        super.tag
          ?: Throwable()
            .stackTrace
            .first { it.className !in fqcnIgnore }
            .let(::createStackElementTag)

    /**
     * Extract the tag which should be used for the message from the `element`. By default this will
     * use the class name without any anonymous class suffixes (e.g., `Foo$1` becomes `Foo`).
     *
     * Note: This will not be called if a [manual tag][.tag] was specified.
     */
    protected open fun createStackElementTag(element: StackTraceElement): String? {
      var tag = element.className.substringAfterLast('.')
      val m = ANONYMOUS_CLASS.matcher(tag)
      if (m.find()) {
        tag = m.replaceAll("")
      }
      // Tag length limit was removed in API 26.
      return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
        tag
      } else {
        tag.substring(0, MAX_TAG_LENGTH)
      }
    }

    /**
     * Break up `message` into maximum-length chunks (if needed) and send to either
     * [Log.println()][Log.println] or [Log.wtf()][Log.wtf] for logging.
     *
     * {@inheritDoc}
     */
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
      if (message.length < MAX_LOG_LENGTH) {
        if (priority == Log.ASSERT) {
          Log.wtf(tag, message)
        } else {
          Log.println(priority, tag, message)
        }
        return
      }

      // Split by line, then ensure each line can fit into Log's maximum length.
      var i = 0
      val length = message.length
      while (i < length) {
        var newline = message.indexOf('\n', i)
        newline = if (newline != -1) newline else length
        do {
          val end = Math.min(newline, i + MAX_LOG_LENGTH)
          val part = message.substring(i, end)
          if (priority == Log.ASSERT) {
            Log.wtf(tag, part)
          } else {
            Log.println(priority, tag, part)
          }
          i = end
        } while (i < newline)
        i++
      }
    }

    companion object {
      private const val MAX_LOG_LENGTH = 4000
      private const val MAX_TAG_LENGTH = 23
      private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
    }
  }

  actual companion object Forest : Tree() {
    /** Log a verbose message with optional format args. */
    @JvmStatic
    override fun v(@NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.v(message, *args) }
    }

    /** Log a verbose exception and a message with optional format args. */
    @JvmStatic
    override fun v(t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.v(t, message, *args) }
    }

    /** Log a verbose exception. */
    @JvmStatic
    override fun v(t: Throwable?) {
      treeArray.forEach { it.v(t) }
    }

    /** Log a debug message with optional format args. */
    @JvmStatic
    override fun d(@NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.d(message, *args) }
    }

    /** Log a debug exception and a message with optional format args. */
    @JvmStatic
    override fun d(t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.d(t, message, *args) }
    }

    /** Log a debug exception. */
    @JvmStatic
    override fun d(t: Throwable?) {
      treeArray.forEach { it.d(t) }
    }

    /** Log an info message with optional format args. */
    @JvmStatic
    override fun i(@NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.i(message, *args) }
    }

    /** Log an info exception and a message with optional format args. */
    @JvmStatic
    override fun i(t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.i(t, message, *args) }
    }

    /** Log an info exception. */
    @JvmStatic
    override fun i(t: Throwable?) {
      treeArray.forEach { it.i(t) }
    }

    /** Log a warning message with optional format args. */
    @JvmStatic
    override fun w(@NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.w(message, *args) }
    }

    /** Log a warning exception and a message with optional format args. */
    @JvmStatic
    override fun w(t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.w(t, message, *args) }
    }

    /** Log a warning exception. */
    @JvmStatic
    override fun w(t: Throwable?) {
      treeArray.forEach { it.w(t) }
    }

    /** Log an error message with optional format args. */
    @JvmStatic
    override fun e(@NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.e(message, *args) }
    }

    /** Log an error exception and a message with optional format args. */
    @JvmStatic
    override fun e(t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.e(t, message, *args) }
    }

    /** Log an error exception. */
    @JvmStatic
    override fun e(t: Throwable?) {
      treeArray.forEach { it.e(t) }
    }

    /** Log an assert message with optional format args. */
    @JvmStatic
    override fun wtf(@NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.wtf(message, *args) }
    }

    /** Log an assert exception and a message with optional format args. */
    @JvmStatic
    override fun wtf(t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.wtf(t, message, *args) }
    }

    /** Log an assert exception. */
    @JvmStatic
    override fun wtf(t: Throwable?) {
      treeArray.forEach { it.wtf(t) }
    }

    /** Log at `priority` a message with optional format args. */
    @JvmStatic
    override fun log(priority: Int, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.log(priority, message, *args) }
    }

    /** Log at `priority` an exception and a message with optional format args. */
    @JvmStatic
    override fun log(priority: Int, t: Throwable?, @NonNls message: String?, vararg args: Any?) {
      treeArray.forEach { it.log(priority, t, message, *args) }
    }

    /** Log at `priority` an exception. */
    @JvmStatic
    override fun log(priority: Int, t: Throwable?) {
      treeArray.forEach { it.log(priority, t) }
    }

    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
      throw AssertionError() // Missing override for log method.
    }

    /**
     * A view into Timber's planted trees as a tree itself. This can be used for injecting a logger
     * instance rather than using static methods or to facilitate testing.
     */
    @Suppress(
      "NOTHING_TO_INLINE", // Kotlin users should reference `Tree.Forest` directly.
      "NON_FINAL_MEMBER_IN_OBJECT", // For japicmp check.
    )
    @JvmStatic
    open inline fun asTree(): Tree = this

    /** Set a one-time tag for use on the next logging call. */
    @JvmStatic
    actual fun tag(tag: String): Tree {
      for (tree in treeArray) {
        tree.explicitTag.set(tag)
      }
      return this
    }

    /** Add a new logging tree. */
    @JvmStatic
    actual fun plant(tree: Tree) {
      require(tree !== this) { "Cannot plant Timber into itself." }
      synchronized(trees) {
        trees.add(tree)
        treeArray = trees.toTypedArray()
      }
    }

    /** Adds new logging trees. */
    @JvmStatic
    actual fun plant(vararg trees: Tree) {
      for (tree in trees) {
        requireNotNull(tree) { "trees contained null" }
        require(tree !== this) { "Cannot plant Timber into itself." }
      }
      synchronized(this.trees) {
        Collections.addAll(this.trees, *trees)
        treeArray = this.trees.toTypedArray()
      }
    }

    /** Remove a planted tree. */
    @JvmStatic
    actual fun uproot(tree: Tree) {
      synchronized(trees) {
        require(trees.remove(tree)) { "Cannot uproot tree which is not planted: $tree" }
        treeArray = trees.toTypedArray()
      }
    }

    /** Remove all planted trees. */
    @JvmStatic
    actual fun uprootAll() {
      synchronized(trees) {
        trees.clear()
        treeArray = emptyArray()
      }
    }

    /** Return a copy of all planted [trees][Tree]. */
    @JvmStatic
    actual fun forest(): List<Tree> {
      synchronized(trees) {
        return unmodifiableList(trees.toList())
      }
    }

    @get:[JvmStatic JvmName("treeCount")]
    actual val treeCount
      get() = treeArray.size

    // Both fields guarded by 'trees'.
    private val trees = ArrayList<Tree>()
    @Volatile private var treeArray = emptyArray<Tree>()
  }
}


================================================
FILE: timber/src/commonMain/kotlin/timber/log/Timber.kt
================================================
package timber.log

/** Logging for lazy people. */
expect class Timber private constructor() {

  /** A facade for handling logging calls. Install instances via [`Timber.plant()`][.plant]. */
  abstract class Tree {

    /** Log a verbose message with optional format args. */
    open fun v(message: String?, vararg args: Any?)

    /** Log a verbose exception and a message with optional format args. */
    open fun v(t: Throwable?, message: String?, vararg args: Any?)

    /** Log a verbose exception. */
    open fun v(t: Throwable?)

    /** Log a debug message with optional format args. */
    open fun d(message: String?, vararg args: Any?)

    /** Log a debug exception and a message with optional format args. */
    open fun d(t: Throwable?, message: String?, vararg args: Any?)

    /** Log a debug exception. */
    open fun d(t: Throwable?)

    /** Log an info message with optional format args. */
    open fun i(message: String?, vararg args: Any?)

    /** Log an info exception and a message with optional format args. */
    open fun i(t: Throwable?, message: String?, vararg args: Any?)

    /** Log an info exception. */
    open fun i(t: Throwable?)

    /** Log a warning message with optional format args. */
    open fun w(message: String?, vararg args: Any?)

    /** Log a warning exception and a message with optional format args. */
    open fun w(t: Throwable?, message: String?, vararg args: Any?)

    /** Log a warning exception. */
    open fun w(t: Throwable?)

    /** Log an error message with optional format args. */
    open fun e(message: String?, vararg args: Any?)

    /** Log an error exception and a message with optional format args. */
    open fun e(t: Throwable?, message: String?, vararg args: Any?)

    /** Log an error exception. */
    open fun e(t: Throwable?)

    /** Log an assert message with optional format args. */
    open fun wtf(message: String?, vararg args: Any?)

    /** Log an assert exception and a message with optional format args. */
    open fun wtf(t: Throwable?, message: String?, vararg args: Any?)

    /** Log an assert exception. */
    open fun wtf(t: Throwable?)

    /** Log at `priority` a message with optional format args. */
    open fun log(priority: Int, message: String?, vararg args: Any?)

    /** Log at `priority` an exception and a message with optional format args. */
    open fun log(priority: Int, t: Throwable?, message: String?, vararg args: Any?)

    /** Log at `priority` an exception. */
    open fun log(priority: Int, t: Throwable?)

    /** Return whether a message at `priority` or `tag` should be logged. */
    protected open fun isLoggable(tag: String?, priority: Int): Boolean

    /** Formats a log message with optional arguments. */
    protected open fun formatMessage(message: String, args: Array<out Any?>): String

    /**
     * Write a log message to its destination. Called for all level-specific methods by default.
     *
     * @param priority Log level. See [Log] for constants.
     * @param tag Explicit or inferred tag. May be `null`.
     * @param message Formatted log message.
     * @param t Accompanying exceptions. May be `null`.
     */
    protected abstract fun log(priority: Int, tag: String?, message: String, t: Throwable?)
  }

  companion object Forest : Tree {

    /** Set a one-time tag for use on the next logging call. */
    fun tag(tag: String): Tree

    /** Add a new logging tree. */
    fun plant(tree: Tree)

    /** Adds new logging trees. */
    fun plant(vararg trees: Tree)

    /** Remove a planted tree. */
    fun uproot(tree: Tree)

    /** Remove all planted trees. */
    fun uprootAll()

    /** Return a copy of all planted [trees][Tree]. */
    fun forest(): List<Tree>

    val treeCount: Int
  }
}


================================================
FILE: timber/src/test/java/timber/log/TimberJavaTest.java
================================================
package timber.log;

import org.junit.Test;

import static org.junit.Assert.fail;

public class TimberJavaTest {
  @SuppressWarnings("ConstantConditions")
  @Test public void nullTree() {
    try {
      Timber.plant((Timber.Tree) null);
      fail();
    } catch (NullPointerException ignored) {
    }
  }

  @SuppressWarnings("ConstantConditions")
  @Test public void nullTreeArray() {
    try {
      Timber.plant((Timber.Tree[]) null);
      fail();
    } catch (NullPointerException ignored) {
    }
    try {
      Timber.plant(new Timber.Tree[] { null });
      fail();
    } catch (IllegalArgumentException ignored) {
    }
  }
}


================================================
FILE: timber/src/test/java/timber/log/TimberTest.kt
================================================
package timber.log

import android.os.Build
import android.util.Log
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.containsExactly
import assertk.assertions.hasMessage
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.message
import assertk.assertions.startsWith
import java.net.ConnectException
import java.net.UnknownHostException
import java.util.ArrayList
import java.util.concurrent.CountDownLatch
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLog
import org.robolectric.shadows.ShadowLog.LogItem

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class TimberTest {
  @Before
  @After
  fun setUpAndTearDown() {
    Timber.uprootAll()
  }

  // NOTE: This class references the line number. Keep it at the top so it does not change.
  @Test
  fun debugTreeCanAlterCreatedTag() {
    Timber.plant(
      object : Timber.DebugTree() {
        override fun createStackElementTag(element: StackTraceElement): String? {
          return super.createStackElementTag(element) + ':'.toString() + element.lineNumber
        }
      }
    )

    Timber.d("Test")

    assertLog().hasDebugMessage("TimberTest:48", "Test").hasNoMoreMessages()
  }

  @Test
  fun recursion() {
    val timber = Timber.asTree()

    assertFailure { Timber.plant(timber) }
      .isInstanceOf<IllegalArgumentException>()
      .hasMessage("Cannot plant Timber into itself.")

    assertFailure {
        @Suppress("RemoveRedundantSpreadOperator") // Explicitly calling vararg overload.
        Timber.plant(*arrayOf(timber))
      }
      .isInstanceOf<IllegalArgumentException>()
      .hasMessage("Cannot plant Timber into itself.")
  }

  @Test
  fun treeCount() {
    // inserts trees and checks if the amount of returned trees matches.
    assertThat(Timber.treeCount).isEqualTo(0)
    for (i in 1 until 50) {
      Timber.plant(Timber.DebugTree())
      assertThat(Timber.treeCount).isEqualTo(i)
    }
    Timber.uprootAll()
    assertThat(Timber.treeCount).isEqualTo(0)
  }

  @Test
  fun forestReturnsAllPlanted() {
    val tree1 = Timber.DebugTree()
    val tree2 = Timber.DebugTree()
    Timber.plant(tree1)
    Timber.plant(tree2)

    assertThat(Timber.forest()).containsExactly(tree1, tree2)
  }

  @Test
  fun forestReturnsAllTreesPlanted() {
    val tree1 = Timber.DebugTree()
    val tree2 = Timber.DebugTree()
    Timber.plant(tree1, tree2)

    assertThat(Timber.forest()).containsExactly(tree1, tree2)
  }

  @Test
  fun uprootThrowsIfMissing() {
    assertFailure { Timber.uproot(Timber.DebugTree()) }
      .isInstanceOf<IllegalArgumentException>()
      .message()
      .isNotNull()
      .startsWith("Cannot uproot tree which is not planted: ")
  }

  @Test
  fun uprootRemovesTree() {
    val tree1 = Timber.DebugTree()
    val tree2 = Timber.DebugTree()
    Timber.plant(tree1)
    Timber.plant(tree2)
    Timber.d("First")
    Timber.uproot(tree1)
    Timber.d("Second")

    assertLog()
      .hasDebugMessage("TimberTest", "First")
      .hasDebugMessage("TimberTest", "First")
      .hasDebugMessage("TimberTest", "Second")
      .hasNoMoreMessages()
  }

  @Test
  fun uprootAllRemovesAll() {
    val tree1 = Timber.DebugTree()
    val tree2 = Timber.DebugTree()
    Timber.plant(tree1)
    Timber.plant(tree2)
    Timber.d("First")
    Timber.uprootAll()
    Timber.d("Second")

    assertLog()
      .hasDebugMessage("TimberTest", "First")
      .hasDebugMessage("TimberTest", "First")
      .hasNoMoreMessages()
  }

  @Test
  fun noArgsDoesNotFormat() {
    Timber.plant(Timber.DebugTree())
    Timber.d("te%st")

    assertLog().hasDebugMessage("TimberTest", "te%st").hasNoMoreMessages()
  }

  @Test
  fun debugTreeTagGeneration() {
    Timber.plant(Timber.DebugTree())
    Timber.d("Hello, world!")

    assertLog().hasDebugMessage("TimberTest", "Hello, world!").hasNoMoreMessages()
  }

  internal inner class ThisIsAReallyLongClassName {
    fun run() {
      Timber.d("Hello, world!")
    }
  }

  @Config(sdk = [25])
  @Test
  fun debugTreeTagTruncation() {
    Timber.plant(Timber.DebugTree())

    ThisIsAReallyLongClassName().run()

    assertLog().hasDebugMessage("TimberTest\$ThisIsAReall", "Hello, world!").hasNoMoreMessages()
  }

  @Config(sdk = [26])
  @Test
  fun debugTreeTagNoTruncation() {
    Timber.plant(Timber.DebugTree())

    ThisIsAReallyLongClassName().run()

    assertLog()
      .hasDebugMessage("TimberTest\$ThisIsAReallyLongClassName", "Hello, world!")
      .hasNoMoreMessages()
  }

  @Suppress("ObjectLiteralToLambda") // Lambdas != anonymous classes.
  @Test
  fun debugTreeTagGenerationStripsAnonymousClassMarker() {
    Timber.plant(Timber.DebugTree())
    object : Runnable {
        override fun run() {
          Timber.d("Hello, world!")

          object : Runnable {
              override fun run() {
                Timber.d("Hello, world!")
              }
            }
            .run()
        }
      }
      .run()

    assertLog()
      .hasDebugMessage("TimberTest\$debugTreeTag", "Hello, world!")
      .hasDebugMessage("TimberTest\$debugTreeTag", "Hello, world!")
      .hasNoMoreMessages()
  }

  @Suppress("ObjectLiteralToLambda") // Lambdas != anonymous classes.
  @Test
  fun debugTreeTagGenerationStripsAnonymousClassMarkerWithInnerSAMLambda() {
    Timber.plant(Timber.DebugTree())
    object : Runnable {
        override fun run() {
          Timber.d("Hello, world!")

          Runnable { Timber.d("Hello, world!") }.run()
        }
      }
      .run()

    assertLog()
      .hasDebugMessage("TimberTest\$debugTreeTag", "Hello, world!")
      .hasDebugMessage("TimberTest\$debugTreeTag", "Hello, world!")
      .hasNoMoreMessages()
  }

  @Suppress("ObjectLiteralToLambda") // Lambdas != anonymous classes.
  @Test
  fun debugTreeTagGenerationStripsAnonymousClassMarkerWithOuterSAMLambda() {
    Timber.plant(Timber.DebugTree())

    Runnable {
        Timber.d("Hello, world!")

        object : Runnable {
            override fun run() {
              Timber.d("Hello, world!")
            }
          }
          .run()
      }
      .run()

    assertLog()
      .hasDebugMessage("TimberTest", "Hello, world!")
      .hasDebugMessage("TimberTest\$debugTreeTag", "Hello, world!")
      .hasNoMoreMessages()
  }

  @Test
  fun debugTreeTagGenerationStripsAnonymousLambdaClassMarker() {
    Timber.plant(Timber.DebugTree())

    val outer = {
      Timber.d("Hello, world!")

      val inner = { Timber.d("Hello, world!") }

      inner()
    }

    outer()

    assertLog()
      .hasDebugMessage("TimberTest", "Hello, world!")
      .hasDebugMessage("TimberTest", "Hello, world!")
      .hasNoMoreMessages()
  }

  @Test
  fun debugTreeTagGenerationForSAMLambdasUsesClassName() {
    Timber.plant(Timber.DebugTree())

    Runnable {
        Timber.d("Hello, world!")

        Runnable { Timber.d("Hello, world!") }.run()
      }
      .run()

    assertLog()
      .hasDebugMessage("TimberTest", "Hello, world!")
      .hasDebugMessage("TimberTest", "Hello, world!")
      .hasNoMoreMessages()
  }

  private class ClassNameThatIsReallyReallyReallyLong {
    init {
      Timber.i("Hello, world!")
    }
  }

  @Test
  fun debugTreeGeneratedTagIsLoggable() {
    Timber.plant(
      object : Timber.DebugTree() {
        private val MAX_TAG_LENGTH = 23

        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
          try {
            assertTrue(Log.isLoggable(tag, priority))
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
              assertTrue(tag!!.length <= MAX_TAG_LENGTH)
            }
          } catch (e: IllegalArgumentException) {
            fail(e.message)
          }

          super.log(priority, tag, message, t)
        }
      }
    )
    ClassNameThatIsReallyReallyReallyLong()
    assertLog().hasInfoMessage("TimberTest\$ClassNameTha", "Hello, world!").hasNoMoreMessages()
  }

  @Test
  fun debugTreeCustomTag() {
    Timber.plant(Timber.DebugTree())
    Timber.tag("Custom").d("Hello, world!")

    assertLog().hasDebugMessage("Custom", "Hello, world!").hasNoMoreMessages()
  }

  @Test
  fun messageWithException() {
    Timber.plant(Timber.DebugTree())
    val datThrowable = truncatedThrowable(NullPointerException::class.java)
    Timber.e(datThrowable, "OMFG!")

    assertExceptionLogged(Log.ERROR, "OMFG!", "java.lang.NullPointerException")
  }

  @Test
  fun exceptionOnly() {
    Timber.plant(Timber.DebugTree())

    Timber.v(truncatedThrowable(IllegalArgumentException::class.java))
    assertExceptionLogged(Log.VERBOSE, null, "java.lang.IllegalArgumentException", "TimberTest", 0)

    Timber.i(truncatedThrowable(NullPointerException::class.java))
    assertExceptionLogged(Log.INFO, null, "java.lang.NullPointerException", "TimberTest", 1)

    Timber.d(truncatedThrowable(UnsupportedOperationException::class.java))
    assertExceptionLogged(
      Log.DEBUG,
      null,
      "java.lang.UnsupportedOperationException",
      "TimberTest",
      2,
    )

    Timber.w(truncatedThrowable(UnknownHostException::class.java))
    assertExceptionLogged(Log.WARN, null, "java.net.UnknownHostException", "TimberTest", 3)

    Timber.e(truncatedThrowable(ConnectException::class.java))
    assertExceptionLogged(Log.ERROR, null, "java.net.ConnectException", "TimberTest", 4)

    Timber.wtf(truncatedThrowable(AssertionError::class.java))
    assertExceptionLogged(Log.ASSERT, null, "java.lang.AssertionError", "TimberTest", 5)
  }

  @Test
  fun exceptionOnlyCustomTag() {
    Timber.plant(Timber.DebugTree())

    Timber.tag("Custom").v(truncatedThrowable(IllegalArgumentException::class.java))
    assertExceptionLogged(Log.VERBOSE, null, "java.lang.IllegalArgumentException", "Custom", 0)

    Timber.tag("Custom").i(truncatedThrowable(NullPointerException::class.java))
    assertExceptionLogged(Log.INFO, null, "java.lang.NullPointerException", "Custom", 1)

    Timber.tag("Custom").d(truncatedThrowable(UnsupportedOperationException::class.java))
    assertExceptionLogged(Log.DEBUG, null, "java.lang.UnsupportedOperationException", "Custom", 2)

    Timber.tag("Custom").w(truncatedThrowable(UnknownHostException::class.java))
    assertExceptionLogged(Log.WARN, null, "java.net.UnknownHostException", "Custom", 3)

    Timber.tag("Custom").e(truncatedThrowable(ConnectException::class.java))
    assertExceptionLogged(Log.ERROR, null, "java.net.ConnectException", "Custom", 4)

    Timber.tag("Custom").wtf(truncatedThrowable(AssertionError::class.java))
    assertExceptionLogged(Log.ASSERT, null, "java.lang.AssertionError", "Custom", 5)
  }

  @Test
  fun exceptionFromSpawnedThread() {
    Timber.plant(Timber.DebugTree())
    val datThrowable = truncatedThrowable(NullPointerException::class.java)
    val latch = CountDownLatch(1)
    object : Thread() {
        override fun run() {
          Timber.e(datThrowable, "OMFG!")
          latch.countDown()
        }
      }
      .start()
    latch.await()
    assertExceptionLogged(
      Log.ERROR,
      "OMFG!",
      "java.lang.NullPointerException",
      "TimberTest\$exceptionFro",
    )
  }

  @Test
  fun nullMessageWithThrowable() {
    Timber.plant(Timber.DebugTree())
    val datThrowable = truncatedThrowable(NullPointerException::class.java)
    Timber.e(datThrowable, null)

    assertExceptionLogged(Log.ERROR, "", "java.lang.NullPointerException")
  }

  @Test
  fun chunkAcrossNewlinesAndLimit() {
    Timber.plant(Timber.DebugTree())
    Timber.d(
      'a'.repeat(3000) + '\n'.toString() + 'b'.repeat(6000) + '\n'.toString() + 'c'.repeat(3000)
    )

    assertLog()
      .hasDebugMessage("TimberTest", 'a'.repeat(3000))
      .hasDebugMessage("TimberTest", 'b'.repeat(4000))
      .hasDebugMessage("TimberTest", 'b'.repeat(2000))
      .hasDebugMessage("TimberTest", 'c'.repeat(3000))
      .hasNoMoreMessages()
  }

  @Test
  fun nullMessageWithoutThrowable() {
    Timber.plant(Timber.DebugTree())
    Timber.d(null as String?)

    assertLog().hasNoMoreMessages()
  }

  @Test
  fun logMessageCallback() {
    val logs = ArrayList<String>()
    Timber.plant(
      object : Timber.DebugTree() {
        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
          logs.add("$priority $tag $message")
        }
      }
    )

    Timber.v("Verbose")
    Timber.tag("Custom").v("Verbose")
    Timber.d("Debug")
    Timber.tag("Custom").d("Debug")
    Timber.i("Info")
    Timber.tag("Custom").i("Info")
    Timber.w("Warn")
    Timber.tag("Custom").w("Warn")
    Timber.e("Error")
    Timber.tag("Custom").e("Error")
    Timber.wtf("Assert")
    Timber.tag("Custom").wtf("Assert")

    assertThat(logs)
      .containsExactly( //
        "2 TimberTest Verbose", //
        "2 Custom Verbose", //
        "3 TimberTest Debug", //
        "3 Custom Debug", //
        "4 TimberTest Info", //
        "4 Custom Info", //
        "5 TimberTest Warn", //
        "5 Custom Warn", //
        "6 TimberTest Error", //
        "6 Custom Error", //
        "7 TimberTest Assert", //
        "7 Custom Assert", //
      )
  }

  @Test
  fun logAtSpecifiedPriority() {
    Timber.plant(Timber.DebugTree())

    Timber.log(Log.VERBOSE, "Hello, World!")
    Timber.log(Log.DEBUG, "Hello, World!")
    Timber.log(Log.INFO, "Hello, World!")
    Timber.log(Log.WARN, "Hello, World!")
    Timber.log(Log.ERROR, "Hello, World!")
    Timber.log(Log.ASSERT, "Hello, World!")

    assertLog()
      .hasVerboseMessage("TimberTest", "Hello, World!")
      .hasDebugMessage("TimberTest", "Hello, World!")
      .hasInfoMessage("TimberTest", "Hello, World!")
      .hasWarnMessage("TimberTest", "Hello, World!")
      .hasErrorMessage("TimberTest", "Hello, World!")
      .hasAssertMessage("TimberTest", "Hello, World!")
      .hasNoMoreMessages()
  }

  @Test
  fun formatting() {
    Timber.plant(Timber.DebugTree())
    Timber.v("Hello, %s!", "World")
    Timber.d("Hello, %s!", "World")
    Timber.i("Hello, %s!", "World")
    Timber.w("Hello, %s!", "World")
    Timber.e("Hello, %s!", "World")
    Timber.wtf("Hello, %s!", "World")

    assertLog()
      .hasVerboseMessage("TimberTest", "Hello, World!")
      .hasDebugMessage("TimberTest", "Hello, World!")
      .hasInfoMessage("TimberTest", "Hello, World!")
      .hasWarnMessage("TimberTest", "Hello, World!")
      .hasErrorMessage("TimberTest", "Hello, World!")
      .hasAssertMessage("TimberTest", "Hello, World!")
      .hasNoMoreMessages()
  }

  @Test
  fun isLoggableControlsLogging() {
    Timber.plant(
      object : Timber.DebugTree() {
        @Suppress("OverridingDeprecatedMember") // Explicitly testing deprecated variant.
        override fun isLoggable(priority: Int): Boolean {
          return priority == Log.INFO
        }
      }
    )
    Timber.v("Hello, World!")
    Timber.d("Hello, World!")
    Timber.i("Hello, World!")
    Timber.w("Hello, World!")
    Timber.e("Hello, World!")
    Timber.wtf("Hello, World!")

    assertLog().hasInfoMessage("TimberTest", "Hello, World!").hasNoMoreMessages()
  }

  @Test
  fun isLoggableTagControlsLogging() {
    Timber.plant(
      object : Timber.DebugTree() {
        override fun isLoggable(tag: String?, priority: Int): Boolean {
          return "FILTER" == tag
        }
      }
    )
    Timber.tag("FILTER").v("Hello, World!")
    Timber.d("Hello, World!")
    Timber.i("Hello, World!")
    Timber.w("Hello, World!")
    Timber.e("Hello, World!")
    Timber.wtf("Hello, World!")

    assertLog().hasVerboseMessage("FILTER", "Hello, World!").hasNoMoreMessages()
  }

  @Test
  fun logsUnknownHostExceptions() {
    Timber.plant(Timber.DebugTree())
    Timber.e(truncatedThrowable(UnknownHostException::class.java), null)

    assertExceptionLogged(Log.ERROR, "", "UnknownHostException")
  }

  @Test
  fun tagIsClearedWhenNotLoggable() {
    Timber.plant(
      object : Timber.DebugTree() {
        override fun isLoggable(tag: String?, priority: Int): Boolean {
          return priority >= Log.WARN
        }
      }
    )
    Timber.tag("NotLogged").i("Message not logged")
    Timber.w("Message logged")

    assertLog().hasWarnMessage("TimberTest", "Message logged").hasNoMoreMessages()
  }

  @Test
  fun logsWithCustomFormatter() {
    Timber.plant(
      object : Timber.DebugTree() {
        override fun formatMessage(message: String, vararg args: Any?): String {
          return String.format("Test formatting: $message", *args)
        }
      }
    )
    Timber.d("Test message logged. %d", 100)

    assertLog().hasDebugMessage("TimberTest", "Test formatting: Test message logged. 100")
  }

  private fun <T : Throwable> truncatedThrowable(throwableClass: Class<T>): T {
    val throwable = throwableClass.newInstance()
    val stackTrace = throwable.stackTrace
    val traceLength = if (stackTrace.size > 5) 5 else stackTrace.size
    throwable.stackTrace = stackTrace.copyOf(traceLength)
    return throwable
  }

  private fun Char.repeat(number: Int) = toString().repeat(number)

  private fun assertExceptionLogged(
    logType: Int,
    message: String?,
    exceptionClassname: String,
    tag: String? = null,
    index: Int = 0,
  ) {
    val logs = getLogs()
    assertThat(logs).hasSize(index + 1)
    val log = logs[index]
    assertThat(log.type).isEqualTo(logType)
    assertThat(log.tag).isEqualTo(tag ?: "TimberTest")

    if (message != null) {
      assertThat(log.msg).startsWith(message)
    }

    assertThat(log.msg).contains(exceptionClassname)
    // We use a low-level primitive that Robolectric doesn't populate.
    assertThat(log.throwable).isNull()
  }

  private fun assertLog(): LogAssert {
    return LogAssert(getLogs())
  }

  private fun getLogs() = ShadowLog.getLogs().filter { it.tag != ROBOLECTRIC_INSTRUMENTATION_TAG }

  private class LogAssert internal constructor(private val items: List<LogItem>) {
    private var index = 0

    fun hasVerboseMessage(tag: String, message: String): LogAssert {
      return hasMessage(Log.VERBOSE, tag, message)
    }

    fun hasDebugMessage(tag: String, message: String): LogAssert {
      return hasMessage(Log.DEBUG, tag, message)
    }

    fun hasInfoMessage(tag: String, message: String): LogAssert {
      return hasMessage(Log.INFO, tag, message)
    }

    fun hasWarnMessage(tag: String, message: String): LogAssert {
      return hasMessage(Log.WARN, tag, message)
    }

    fun hasErrorMessage(tag: String, message: String): LogAssert {
      return hasMessage(Log.ERROR, tag, message)
    }

    fun hasAssertMessage(tag: String, message: String): LogAssert {
      return hasMessage(Log.ASSERT, tag, message)
    }

    private fun hasMessage(priority: Int, tag: String, message: String): LogAssert {
      val item = items[index++]
      assertThat(item.type).isEqualTo(priority)
      assertThat(item.tag).isEqualTo(tag)
      assertThat(item.msg).isEqualTo(message)
      return this
    }

    fun hasNoMoreMessages() {
      assertThat(items).hasSize(index)
    }
  }

  private companion object {
    private const val ROBOLECTRIC_INSTRUMENTATION_TAG = "MonitoringInstr"
  }
}


================================================
FILE: timber-lint/build.gradle
================================================
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'org.jetbrains.kotlin.kapt'
apply plugin: 'com.android.lint'
apply plugin: 'com.gradleup.tapmoc'

tapmoc {
  java(17)
}

lint {
  baseline = file("lint-baseline.xml")
}

dependencies {
  compileOnly libs.lint.api
  compileOnly libs.lint.checks
  compileOnly libs.auto.annotations
  kapt libs.auto.service
  testImplementation libs.junit
  testImplementation libs.lint.core
  testImplementation libs.lint.tests
  testImplementation libs.junit
}


================================================
FILE: timber-lint/gradle.properties
================================================
# needed so that :timber:prepareLintJarForPublish can succeed
# Remove when the bug described in https://issuetracker.google.com/issues/161727305 is fixed
kotlin.stdlib.default.dependency=false


================================================
FILE: timber-lint/lint-baseline.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<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">

    <issue
        id="LintImplTextFormat"
        message="&quot;format()&quot; looks like a call; surround with backtics in string to display as symbol, e.g. \`format()\`"
        errorLine1="      explanation = &quot;Since Timber handles String.format automatically, you may not use String#format().&quot;,"
        errorLine2="                                                                                              ~~~~~~~~">
        <location
            file="src/main/java/timber/lint/WrongTimberUsageDetector.kt"
            line="760"
            column="95"/>
    </issue>

    <issue
        id="LintImplTextFormat"
        message="&quot;format()&quot; looks like a call; surround with backtics in string to display as symbol, e.g. \`format()\`"
        errorLine1="      explanation = &quot;Since Timber handles String#format() automatically, use this instead of String concatenation.&quot;,"
        errorLine2="                                                 ~~~~~~~~">
        <location
            file="src/main/java/timber/lint/WrongTimberUsageDetector.kt"
            line="778"
            column="50"/>
    </issue>

    <issue
        id="LintImplDollarEscapes"
        message="In unit tests, use the fullwidth dollar sign, `$`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `$` as a `$`."
        errorLine1="                |     Timber.d(&quot;${&quot;$&quot;}{foo}bar&quot;)"
        errorLine2="                                ~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="684"
            column="33"/>
    </issue>

    <issue
        id="LintImplDollarEscapes"
        message="In unit tests, use the fullwidth dollar sign, `$`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `$` as a `$`."
        errorLine1="                |     Timber.d(&quot;foo${&quot;$&quot;}bar&quot;)"
        errorLine2="                                   ~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="733"
            column="36"/>
    </issue>

    <issue
        id="LintImplDollarEscapes"
        message="In unit tests, use the fullwidth dollar sign, `$`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `$` as a `$`."
        errorLine1="                |     Timber.d(&quot;${&quot;$&quot;}{foo}${&quot;$&quot;}bar&quot;)"
        errorLine2="                                ~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="784"
            column="33"/>
    </issue>

    <issue
        id="LintImplDollarEscapes"
        message="In unit tests, use the fullwidth dollar sign, `$`, instead of `$`, to avoid having to use cumbersome escapes. Lint will treat a `$` as a `$`."
        errorLine1="                |     Timber.d(if(true) &quot;Hello, ${&quot;$&quot;}{s}&quot; else &quot;Bye&quot;)"
        errorLine2="                                                ~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="833"
            column="49"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="40"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="50"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="100"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="110"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="159"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="168"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="217"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="226"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="277"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="287"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="332"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="343"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="388"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="399"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="431"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="461"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="472"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="493"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="502"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="521"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="530"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="              |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                    ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="553"
            column="21"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="              |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                    ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="564"
            column="21"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="587"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="598"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="642"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="652"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="675"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="686"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="724"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="735"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="774"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="786"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="824"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="835"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="864"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="874"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="905"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="915"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="946"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="956"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="987"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="997"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1026"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1036"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1059"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1069"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1100"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1110"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1141"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1151"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1183"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1194"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1238"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1249"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1295"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1307"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1330"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1340"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1366"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1379"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1404"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1416"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1441"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1453"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1477"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1488"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1533"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1544"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1589"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1600"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1623"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1633"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1655"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1665"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                |}&quot;&quot;&quot;.trimMargin()"
        errorLine2="                      ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1687"
            column="23"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                &quot;&quot;&quot;.trimMargin()"
        errorLine2="                    ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1715"
            column="21"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="                &quot;&quot;&quot;.trimMargin()"
        errorLine2="                    ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1730"
            column="21"/>
    </issue>

    <issue
        id="LintImplTrimIndent"
        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"
        errorLine1="              &quot;&quot;&quot;.trimMargin()"
        errorLine2="                  ~~~~~~~~~~~~">
        <location
            file="src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt"
            line="1767"
            column="19"/>
    </issue>

    <issue
        id="UElementAsPsi"
        message="Do not use `UElement` as `PsiElement`"
        errorLine1="    if (expression is PsiMethodCallExpression) {"
        errorLine2="        ~~~~~~~~~~">
        <location
            file="src/main/java/timber/lint/WrongTimberUsageDetector.kt"
            line="283"
            column="9"/>
    </issue>

    <issue
        id="UElementAsPsi"
        message="Do not use `UElement` as `PsiElement`"
        errorLine1="    } else if (expression is PsiLiteralExpression) {"
        errorLine2="               ~~~~~~~~~~">
        <location
            file="src/main/java/timber/lint/WrongTimberUsageDetector.kt"
            line="290"
            column="16"/>
    </issue>

</issues>


================================================
FILE: timber-lint/src/main/java/timber/lint/TimberIssueRegistry.kt
================================================
package timber.lint

import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
import com.google.auto.service.AutoService

@Suppress("UnstableApiUsage", "unused")
@AutoService(value = [IssueRegistry::class])
class TimberIssueRegistry : IssueRegistry() {
  override val issues: List<Issue>
    get() = WrongTimberUsageDetector.issues.asList()

  override val api: Int
    get() = CURRENT_API

  /** works with Studio 4.0 or later; see [com.android.tools.lint.detector.api.describeApi] */
  override val minApi: Int
    get() = 7

  override val vendor =
    Vendor(
      vendorName = "JakeWharton/timber",
      identifier = "com.jakewharton.timber:timber:{version}",
      feedbackUrl = "https://github.com/JakeWharton/timber/issues",
    )
}


================================================
FILE: timber-lint/src/main/java/timber/lint/WrongTimberUsageDetector.kt
================================================
package timber.lint

import com.android.tools.lint.checks.StringFormatDetector
import com.android.tools.lint.client.api.JavaEvaluator
import com.android.tools.lint.client.api.TYPE_BOOLEAN
import com.android.tools.lint.client.api.TYPE_BOOLEAN_WRAPPER
import com.android.tools.lint.client.api.TYPE_BYTE
import com.android.tools.lint.client.api.TYPE_BYTE_WRAPPER
import com.android.tools.lint.client.api.TYPE_CHAR
import com.android.tools.lint.client.api.TYPE_DOUBLE
import com.android.tools.lint.client.api.TYPE_DOUBLE_WRAPPER
import com.android.tools.lint.client.api.TYPE_FLOAT
import com.android.tools.lint.client.api.TYPE_FLOAT_WRAPPER
import com.android.tools.lint.client.api.TYPE_INT
import com.android.tools.lint.client.api.TYPE_INTEGER_WRAPPER
import com.android.tools.lint.client.api.TYPE_LONG
import com.android.tools.lint.client.api.TYPE_LONG_WRAPPER
import com.android.tools.lint.client.api.TYPE_NULL
import com.android.tools.lint.client.api.TYPE_OBJECT
import com.android.tools.lint.client.api.TYPE_SHORT
import com.android.tools.lint.client.api.TYPE_SHORT_WRAPPER
import com.android.tools.lint.client.api.TYPE_STRING
import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
import com.android.tools.lint.detector.api.Category.Companion.MESSAGES
import com.android.tools.lint.detector.api.ConstantEvaluator.evaluateString
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Detector.UastScanner
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Incident
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE
import com.android.tools.lint.detector.api.Severity.ERROR
import com.android.tools.lint.detector.api.Severity.WARNING
import com.android.tools.lint.detector.api.isKotlin
import com.android.tools.lint.detector.api.isString
import com.android.tools.lint.detector.api.minSdkLessThan
import com.android.tools.lint.detector.api.skipParentheses
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
import java.lang.Byte
import java.lang.Double
import java.lang.Float
import java.lang.IllegalStateException
import java.lang.Long
import java.lang.Short
import java.util.Calendar
import java.util.Date
import java.util.regex.Pattern
import kotlin.Any
import kotlin.Boolean
import kotlin.Number
import kotlin.String
import kotlin.Throwable
import kotlin.arrayOf
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.ULiteralExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.evaluateString
import org.jetbrains.uast.isInjectionHost
import org.jetbrains.uast.util.isMethodCall

class WrongTimberUsageDetector : Detector(), UastScanner {
  override fun getApplicableMethodNames() = listOf("tag", "format", "v", "d", "i", "w", "e", "wtf")

  override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
    val methodName = node.methodName
    val evaluator = context.evaluator

    if (
      "format" == methodName &&
        (evaluator.isMemberInClass(method, "java.lang.String") ||
          evaluator.isMemberInClass(method, "kotlin.text.StringsKt__StringsJVMKt"))
    ) {
      checkNestedStringFormat(context, node)
      return
    }
    if ("tag" == methodName && evaluator.isMemberInClass(method, "timber.log.Timber")) {
      checkTagLengthIfMinSdkLessThan26(context, node)
    }
    if (evaluator.isMemberInClass(method, "android.util.Log")) {
      context.report(
        Incident(
          issue = ISSUE_LOG,
          scope = node,
          location = context.getLocation(node),
          message = "Using 'Log' instead of 'Timber'",
          fix = quickFixIssueLog(node),
        )
      )
      return
    }
    // Handles Timber.X(..) and Timber.tag(..).X(..) where X in (v|d|i|w|e|wtf).
    if (isTimberLogMethod(method, evaluator)) {
      checkMethodArguments(context, node)
      checkFormatArguments(context, node)
      checkExceptionLogging(context, node)
    }
  }

  private fun isTimberLogMethod(method: PsiMethod, evaluator: JavaEvaluator): Boolean {
    return evaluator.isMemberInClass(method, "timber.log.Timber") ||
      evaluator.isMemberInClass(method, "timber.log.Timber.Companion") ||
      evaluator.isMemberInClass(method, "timber.log.Timber.Tree")
  }

  private fun checkNestedStringFormat(context: JavaContext, call: UCallExpression) {
    var current: UElement? = call
    while (true) {
      current = skipParentheses(current!!.uastParent)
      if (current == null || current is UMethod) {
        // Reached AST root or code block node; String.format not inside Timber.X(..).
        return
      }
      if (current.isMethodCall()) {
        val psiMethod = (current as UCallExpression).resolve()
        if (
          psiMethod != null &&
            Pattern.matches(TIMBER_TREE_LOG_METHOD_REGEXP, psiMethod.name) &&
            isTimberLogMethod(psiMethod, context.evaluator)
        ) {
          context.report(
            Incident(
              issue = ISSUE_FORMAT,
              scope = call,
              location = context.getLocation(call),
              message = "Using 'String#format' inside of 'Timber'",
              fix = quickFixIssueFormat(call),
            )
          )
          return
        }
      }
    }
  }

  private fun checkTagLengthIfMinSdkLessThan26(context: JavaContext, call: UCallExpression) {
    val argument = call.valueArguments[0]
    val tag = evaluateString(context, argument, true)
    if (tag != null && tag.length > 23) {
      context.report(
        Incident(
          issue = ISSUE_TAG_LENGTH,
          scope = argument,
          location = context.getLocation(argument),
          message = "The logging tag can be at most 23 characters, was ${tag.length} ($tag)",
          fix = quickFixIssueTagLength(argument, tag),
        ),
        // As of API 26, Log tags are no longer limited to 23 chars.
        constraint = minSdkLessThan(26),
      )
    }
  }

  private fun checkFormatArguments(context: JavaContext, call: UCallExpression) {
    val arguments = call.valueArguments
    val numArguments = arguments.size
    if (numArguments == 0) {
      return
    }

    var startIndexOfArguments = 1
    var formatStringArg = arguments[0]
    if (isSubclassOf(context, formatStringArg, Throwable::class.java)) {
      if (numArguments == 1) {
        return
      }
      formatStringArg = arguments[1]
      startIndexOfArguments++
    }

    val formatString =
      evaluateString(context, formatStringArg, false)
        ?: return // We passed for example a method call

    val formatArgumentCount = getFormatArgumentCount(formatString)
    val passedArgCount = numArguments - startIndexOfArguments
    if (formatArgumentCount < passedArgCount) {
      context.report(
        Incident(
          issue = ISSUE_ARG_COUNT,
          scope = call,
          location = context.getLocation(call),
          message =
            "Wrong argument count, format string `${formatString}` requires `${formatArgumentCount}` but format call supplies `${passedArgCount}`",
        )
      )
      return
    }

    if (formatArgumentCount == 0) {
      return
    }

    val types = getStringArgumentTypes(formatString)
    var argument: UExpression? = null
    var argumentIndex = startIndexOfArguments
    var valid: Boolean
    for (i in types.indices) {
      val formatType = types[i]
      if (argumentIndex != numArguments) {
        argument = arguments[argumentIndex++]
      } else {
        context.report(
          Incident(
            issue = ISSUE_ARG_COUNT,
            scope = call,
            location = context.getLocation(call),
            message =
              "Wrong argument count, format string `${formatString}` requires `${formatArgumentCount}` but format call supplies `${passedArgCount}`",
          )
        )
      }

      val type = getType(argument) ?: continue
      val last = formatType.last()
      if (formatType.length >= 2 && formatType[formatType.length - 2].lowercaseChar() == 't') {
        // Date time conversion.
        when (last) {
          'H',
          'I',
          'k',
          'l',
          'M',
          'S',
          'L',
          'N',
          'p',
          'z',
          'Z',
          's',
          'Q', // time
          'B',
          'b',
          'h',
          'A',
          'a',
          'C',
          'Y',
          'y',
          'j',
          'm',
          'd',
          'e', // date
          'R',
          'T',
          'r',
          'D',
          'F',
          'c' -> { // date/time
            valid =
              type == Integer.TYPE ||
                type == Calendar::class.java ||
                type == Date::class.java ||
                type == Long.TYPE
            if (!valid) {
              context.report(
                Incident(
                  issue = ISSUE_ARG_TYPES,
                  scope = call,
                  location = context.getLocation(argument),
                  message =
                    "Wrong argument type for date formatting argument '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)",
                )
              )
            }
          }
          else -> {
            context.report(
              Incident(
                issue = ISSUE_FORMAT,
                scope = call,
                location = context.getLocation(argument),
                message =
                  "Wrong suffix for date format '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)",
              )
            )
          }
        }
        continue
      }

      valid =
        when (last) {
          'b',
          'B' -> type == java.lang.Boolean.TYPE
          'x',
          'X',
          'd',
          'o',
          'e',
          'E',
          'f',
          'g',
          'G',
          'a',
          'A' -> {
            type == Integer.TYPE ||
              type == Float.TYPE ||
              type == Double.TYPE ||
              type == Long.TYPE ||
              type == Byte.TYPE ||
              type == Short.TYPE
          }
          'c',
          'C' -> type == Character.TYPE
          'h',
          'H' -> type != java.lang.Boolean.TYPE && !Number::class.java.isAssignableFrom(type)
          's',
          'S' -> true
          else -> true
        }
      if (!valid) {
        context.report(
          Incident(
            issue = ISSUE_ARG_TYPES,
            scope = call,
            location = context.getLocation(argument),
            message =
              "Wrong argument type for formatting argument '#${i + 1}' in `${formatString}`: conversion is '`${formatType}`', received `${type.simpleName}` (argument #${startIndexOfArguments + i + 1} in method call)",
          )
        )
      }
    }
  }

  private fun getType(expression: UExpression?): Class<*>? {
    if (expression == null) {
      return null
    }
    if (expression is PsiMethodCallExpression) {
      val call = expression as PsiMethodCallExpression
      val method = call.resolveMethod() ?: return null
      val methodName = method.name
      if (methodName == GET_STRING_METHOD) {
        return String::class.java
      }
    } else if (expression is PsiLiteralExpression) {
      val literalExpression = expression as PsiLiteralExpression
      val expressionType = literalExpression.type
      when {
        isString(expressionType!!) -> return String::class.java
        expressionType === PsiTypes.intType() -> return Integer.TYPE
        expressionType === PsiTypes.floatType() -> return Float.TYPE
        expressionType === PsiTypes.charType() -> return Character.TYPE
        expressionType === PsiTypes.booleanType() -> return java.lang.Boolean.TYPE
        expressionType === PsiTypes.nullType() -> return Any::class.java
      }
    }

    val type = expression.getExpressionType()
    if (type != null) {
      val typeClass = getTypeClass(type)
      return typeClass ?: Any::class.java
    }

    return null
  }

  private fun getTypeClass(type: PsiType?): Class<*>? {
    return when (type?.canonicalText) {
      null -> null
      TYPE_STRING,
      "String" -> String::class.java
      TYPE_INT -> Integer.TYPE
      TYPE_BOOLEAN -> java.lang.Boolean.TYPE
      TYPE_NULL -> Object::class.java
      TYPE_LONG -> Long.TYPE
      TYPE_FLOAT -> Float.TYPE
      TYPE_DOUBLE -> Double.TYPE
      TYPE_CHAR -> Character.TYPE
      TYPE_OBJECT -> null
      TYPE_INTEGER_WRAPPER,
      TYPE_SHORT_WRAPPER,
      TYPE_BYTE_WRAPPER,
      TYPE_LONG_WRAPPER -> Integer.TYPE
      TYPE_FLOAT_WRAPPER,
      TYPE_DOUBLE_WRAPPER -> Float.TYPE
      TYPE_BOOLEAN_WRAPPER -> java.lang.Boolean.TYPE
      TYPE_BYTE -> Byte.TYPE
      TYPE_SHORT -> Short.TYPE
      "Date",
      "java.util.Date" -> Date::class.java
      "Calendar",
      "java.util.Calendar" -> Calendar::class.java
      "BigDecimal",
      "java.math.BigDecimal" -> Float.TYPE
      "BigInteger",
      "java.math.BigInteger" -> Integer.TYPE
      else -> null
    }
  }

  private fun isSubclassOf(context: JavaContext, expression: UExpression, cls: Class<*>): Boolean {
    val expressionType = expression.getExpressionType()
    if (expressionType is PsiClassType) {
      return context.evaluator.extendsClass(expressionType.resolve(), cls.name, false)
    }
    return false
  }

  private fun getStringArgumentTypes(formatString: String): List<String> {
    val types = mutableListOf<String>()
    val matcher = StringFormatDetector.FORMAT.matcher(formatString)
    var index = 0
    var prevIndex = 0

    while (true) {
      if (matcher.find(index)) {
        val matchStart = matcher.start()
        while (prevIndex < matchStart) {
          val c = formatString[prevIndex]
          if (c == '\\') {
            prevIndex++
          }
          prevIndex++
        }
        if (prevIndex > matchStart) {
          index = prevIndex
          continue
        }

        index = matcher.end()
        val str = formatString.substring(matchStart, matcher.end())
        if ("%%" == str || "%n" == str) {
          continue
        }
        val time = matcher.group(5)
        types +=
          if ("t".equals(time, ignoreCase = true)) {
            time + matcher.group(6)
          } else {
            matcher.group(6)
          }
      } else {
        break
      }
    }
    return types
  }

  private fun getFormatArgumentCount(s: String): Int {
    val matcher = StringFormatDetector.FORMAT.matcher(s)
    var index = 0
    var prevIndex = 0
    var nextNumber = 1
    var max = 0
    while (true) {
      if (matcher.find(index)) {
        val value = matcher.group(6)
        if ("%" == value || "n" == value) {
          index = matcher.end()
          continue
        }
        val matchStart = matcher.start()
        while (prevIndex < matchStart) {
          val c = s[prevIndex]
          if (c == '\\') {
            prevIndex++
          }
          prevIndex++
        }
        if (prevIndex > matchStart) {
          index = prevIndex
          continue
        }

        var number: Int
        var numberString = matcher.group(1)
        if (numberString != null) {
          // Strip off trailing $
          numberString = numberString.substring(0, numberString.length - 1)
          number = numberString.toInt()
          nextNumber = number + 1
        } else {
          number = nextNumber++
        }
        if (number > max) {
          max = number
        }
        index = matcher.end()
      } else {
        break
      }
    }
    return max
  }

  private fun checkMethodArguments(context: JavaContext, call: UCallExpression) {
    call.valueArguments.forEachIndexed loop@{ i, argument ->
      if (checkElement(context, call, argument)) return@loop

      if (i > 0 && isSubclassOf(context, argument, Throwable::class.java)) {
        context.report(
          Incident(
            issue = ISSUE_THROWABLE,
            scope = call,
            location = context.getLocation(call),
            message = "Throwable should be first argument",
            fix = quickFixIssueThrowable(call, call.valueArguments, argument),
          )
        )
      }
    }
  }

  private fun checkExceptionLogging(context: JavaContext, call: UCallExpression) {
    val arguments = call.valueArguments
    val numArguments = arguments.size

    // Find the throwable and message arguments by their type, not by their position.
    val throwableArgument = arguments.firstOrNull {
      isSubclassOf(context, it, Throwable::class.java)
    }

    // Find an argument that is either a String OR a literal null expression.
    val messageArgument = arguments.firstOrNull {
      val type = it.getExpressionType()
      (type != null && isString(type)) || (it is ULiteralExpression && it.isNull)
    }

    // Handles overloads like Timber.d(t, "message").
    if (throwableArgument != null && messageArgument != null) {
      // Check for the common mistake of explicitly logging the exception's own message.
      if (isLoggingExceptionMessage(context, messageArgument)) {
        context.report(
          Incident(
            issue = ISSUE_EXCEPTION_LOGGING,
            scope = messageArgument,
            location = context.getLocation(call),
            message = "Explicitly logging exception message is redundant",
            fix = quickFixRemoveRedundantArgument(messageArgument),
          )
        )
        return
      }

      // Check if the argument is a literal null, which is a clear issue.
      if (messageArgument is ULiteralExpression && messageArgument.isNull) {
        context.report(
          Incident(
            issue = ISSUE_EXCEPTION_LOGGING,
            scope = messageArgument,
            location = context.getLocation(call),
            message = "Use single-argument log method instead of null/empty message",
            fix = quickFixRemoveRedundantArgument(messageArgument),
          )
        )
        return
      }

      // If it's not null, then try to evaluate it as a string.
      val messageValue = evaluateString(context, messageArgument, true)

      // If we could determine the string's value (i.e., it's a literal or constant)...
      if (messageValue != null) {
        // ...then we can safely check if it's empty and report an issue.
        if (messageValue.isEmpty()) {
          context.report(
            Incident(
              issue = ISSUE_EXCEPTION_LOGGING,
              scope = messageArgument,
              location = context.getLocation(call),
              message = "Use single-argument log method instead of null/empty message",
              fix = quickFixRemoveRedundantArgument(messageArgument),
            )
          )
        }
      }
      // If messageValue is null, the argument is a variable or method call. We intentionally
      // do nothing in this case to avoid false positives.

      // Handles single-argument overloads like Timber.d("message").
    } else if (numArguments == 1 && throwableArgument == null && messageArgument != null) {
      if (isLoggingExceptionMessage(context, messageArgument)) {
        context.report(
          Incident(
            issue = ISSUE_EXCEPTION_LOGGING,
            scope = messageArgument,
            location = context.getLocation(call),
            message = "Explicitly logging exception message is redundant",
            fix = quickFixReplaceMessageWithThrowable(messageArgument),
          )
        )
      }
    }
  }

  private fun isLoggingExceptionMessage(context: JavaContext, arg: UExpression): Boolean {
    if (arg !is UQualifiedReferenceExpression) {
      return false
    }

    val psi = arg.sourcePsi
    if (psi != null && isKotlin(psi.language)) {
      return isPropertyOnSubclassOf(context, arg, "message", Throwable::class.java)
    }

    val selector = arg.selector

    // what other UExpressions could be a selector?
    return if (selector !is UCallExpression) {
      false
    } else
      isCallFromMethodInSubclassOf(
        context = context,
        call = selector,
        methodName = "getMessage",
        classType = Throwable::class.java,
      )
  }

  private fun isCallFromMethodInSubclassOf(
    context: JavaContext,
    call: UCallExpression,
    methodName: String,
    classType: Class<*>,
  ): Boolean {
    val method = call.resolve()
    return method != null &&
      methodName == call.methodName &&
      context.evaluator.isMemberInSubClassOf(method, classType.canonicalName, false)
  }

  private fun isPropertyOnSubclassOf(
    context: JavaContext,
    expression: UQualifiedReferenceExpression,
    propertyName: String,
    classType: Class<*>,
  ): Boolean {
    return isSubclassOf(context, expression.receiver, classType) &&
      expression.selector.asSourceString() == propertyName
  }

  private fun checkElement(
    context: JavaContext,
    call: UCallExpression,
    element: UElement?,
  ): Boolean {
    if (element is UBinaryExpression) {
      val operator = element.operator
      if (operator === UastBinaryOperator.PLUS || operator === UastBinaryOperator.PLUS_ASSIGN) {
        val argumentType = getType(element)
        if (argumentType == String::class.java) {
          if (element.leftOperand.isInjectionHost() && element.rightOperand.isInjectionHost()) {
            return false
          }
          context.report(
            Incident(
              issue = ISSUE_BINARY,
              scope = call,
              location = context.getLocation(element),
              message = "Replace String concatenation with Timber's string formatting",
              fix = quickFixIssueBinary(element),
            )
          )
          return true
        }
      }
    } else if (element is UIfExpression) {
      return checkConditionalUsage(context, call, element)
    }
    return false
  }

  private fun checkConditionalUsage(
    context: JavaContext,
    call: UCallExpression,
    element: UElement,
  ): Boolean {
    return if (element is UIfExpression) {
      if (checkElement(context, call, element.thenExpression)) {
        false
      } else {
        checkElement(context, call, element.elseExpression)
      }
    } else {
      false
    }
  }

  private fun quickFixIssueLog(logCall: UCallExpression): LintFix {
    val arguments = logCall.valueArguments
    val methodName = logCall.methodName
    val tag = arguments[0]

    // 1st suggestion respects author's tag preference.
    // 2nd suggestion drops it (Timber defaults to calling class name).
    var fixSource1 = "Timber.tag(${tag.asSourceString()})."
    var fixSource2 = "Timber."

    when (arguments.size) {
      2 -> {
        val msgOrThrowable = arguments[1]
        fixSource1 += "$methodName(${msgOrThrowable.asSourceString()})"
        fixSource2 += "$methodName(${msgOrThrowable.asSourceString()})"
      }
      3 -> {
        val msg = arguments[1]
        val throwable = arguments[2]
        fixSource1 += "$methodName(${throwable.sourcePsi?.text}, ${msg.asSourceString()})"
        fixSource2 += "$methodName(${throwable.sourcePsi?.text}, ${msg.asSourceString()})"
      }
      else -> {
        throw IllegalStateException("android.util.Log overloads should have 2 or 3 arguments")
      }
    }

    val logCallSource = logCall.uastParent!!.sourcePsi?.text
    return fix()
      .group()
      .add(
        fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource1).build()
      )
      .add(
        fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource2).build()
      )
      .build()
  }

  private fun quickFixIssueFormat(stringFormatCall: UCallExpression): LintFix {
    // Handles:
    // 1) String.format(..)
    // 2) format(...) [static import]
    val callReceiver = stringFormatCall.receiver
    var callSourceString = if (callReceiver == null) "" else "${callReceiver.asSourceString()}."
    callSourceString += stringFormatCall.methodName

    return fix()
      .name("Remove String.format(...)")
      .composite() //
      // Delete closing parenthesis of String.format(...)
      .add(fix().replace().pattern("$callSourceString\\(.*(\\))").with("").build())
      // Delete "String.format("
      .add(fix().replace().text("$callSourceString(").with("").build())
      .build()
  }

  private fun quickFixIssueThrowable(
    call: UCallExpression,
    arguments: List<UExpression>,
    throwable: UExpression,
  ): LintFix {
    val rearrangedArgs = buildString {
      append(throwable.asSourceString())
      arguments.forEach { arg ->
        if (arg !== throwable) {
          append(", ${arg.asSourceString()}")
        }
      }
    }
    return fix()
      .replace()
      .pattern("\\." + call.methodName + "\\((.*)\\)")
      .with(rearrangedArgs)
      .build()
  }

  private fun quickFixIssueBinary(binaryExpression: UBinaryExpression): LintFix {
    val leftOperand = binaryExpression.leftOperand
    val rightOperand = binaryExpression.rightOperand
    val isLeftLiteral = leftOperand.isInjectionHost()
    val isRightLiteral = rightOperand.isInjectionHost()

    // "a" + "b" => "ab"
    if (isLeftLiteral && isRightLiteral) {
      return fix()
        .replace()
        .text(binaryExpression.asSourceString())
        .with("\"${binaryExpression.evaluateString()}\"")
        .build()
    }

    val args: String =
      when {
        isLeftLiteral -> {
          "\"${leftOperand.evaluateString()}%s\", ${rightOperand.asSourceString()}"
        }
        isRightLiteral -> {
          "\"%s${rightOperand.evaluateString()}\", ${leftOperand.asSourceString()}"
        }
        else -> {
          "\"%s%s\", ${leftOperand.asSourceString()}, ${rightOperand.asSourceString()}"
        }
      }
    return fix().replace().text(binaryExpression.asSourceString()).with(args).build()
  }

  private fun quickFixIssueTagLength(argument: UExpression, tag: String): LintFix {
    val numCharsToTrim = tag.length - 23
    return fix()
      .replace()
      .name("Strip last " + if (numCharsToTrim == 1) "char" else "$numCharsToTrim chars")
      .text(argument.asSourceString())
      .with("\"${tag.substring(0, 23)}\"")
      .build()
  }

  private fun quickFixRemoveRedundantArgument(arg: UExpression): LintFix {
    return fix()
      .replace()
      .name("Remove redundant argument")
      .text(", ${arg.asSourceString()}")
      .with("")
      .build()
  }

  private fun quickFixReplaceMessageWithThrowable(arg: UExpression): LintFix {
    // guaranteed based on callers of this method
    val receiver = (arg as UQualifiedReferenceExpression).receiver
    return fix()
      .replace()
      .name("Replace message with throwable")
      .text(arg.asSourceString())
      .with(receiver.asSourceString())
      .build()
  }

  companion object {
    private const val GET_STRING_METHOD = "getString"
    private const val TIMBER_TREE_LOG_METHOD_REGEXP = "(v|d|i|w|e|wtf)"

    val ISSUE_LOG =
      Issue.create(
        id = "LogNotTimber",
        briefDescription = "Logging call to Log instead of Timber",
        explanation =
          "Since Timber is included in the project, it is likely that calls to Log should instead be going to Timber.",
        category = MESSAGES,
        priority = 5,
        severity = WARNING,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_FORMAT =
      Issue.create(
        id = "StringFormatInTimber",
        briefDescription = "Logging call with Timber contains String#format()",
        explanation =
          "Since Timber handles String.format automatically, you may not use String#format().",
        category = MESSAGES,
        priority = 5,
        severity = WARNING,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_THROWABLE =
      Issue.create(
        id = "ThrowableNotAtBeginning",
        briefDescription = "Exception in Timber not at the beginning",
        explanation = "In Timber you have to pass a Throwable at the beginning of the call.",
        category = MESSAGES,
        priority = 5,
        severity = WARNING,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_BINARY =
      Issue.create(
        id = "BinaryOperationInTimber",
        briefDescription = "Use String#format()",
        explanation =
          "Since Timber handles String#format() automatically, use this instead of String concatenation.",
        category = MESSAGES,
        priority = 5,
        severity = WARNING,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_ARG_COUNT =
      Issue.create(
        id = "TimberArgCount",
        briefDescription = "Formatting argument types incomplete or inconsistent",
        explanation =
          "When a formatted string takes arguments, you need to pass at least that amount of arguments to the formatting call.",
        category = MESSAGES,
        priority = 9,
        severity = ERROR,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_ARG_TYPES =
      Issue.create(
        id = "TimberArgTypes",
        briefDescription = "Formatting string doesn't match passed arguments",
        explanation =
          "The argument types that you specified in your formatting string does not match the types of the arguments that you passed to your formatting call.",
        category = MESSAGES,
        priority = 9,
        severity = ERROR,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_TAG_LENGTH =
      Issue.create(
        id = "TimberTagLength",
        briefDescription = "Too Long Log Tags",
        explanation = "Log tags are only allowed to be at most" + " 23 tag characters long.",
        category = CORRECTNESS,
        priority = 5,
        severity = ERROR,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )
    val ISSUE_EXCEPTION_LOGGING =
      Issue.create(
        id = "TimberExceptionLogging",
        briefDescription = "Exception Logging",
        explanation =
          "Explicitly including the exception message is redundant when supplying an exception to log.",
        category = CORRECTNESS,
        priority = 3,
        severity = WARNING,
        implementation = Implementation(WrongTimberUsageDetector::class.java, JAVA_FILE_SCOPE),
      )

    val issues =
      arrayOf(
        ISSUE_LOG,
        ISSUE_FORMAT,
        ISSUE_THROWABLE,
        ISSUE_BINARY,
        ISSUE_ARG_COUNT,
        ISSUE_ARG_TYPES,
        ISSUE_TAG_LENGTH,
        ISSUE_EXCEPTION_LOGGING,
      )
  }
}


================================================
FILE: timber-lint/src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt
================================================
package timber.lint

import com.android.tools.lint.checks.infrastructure.TestFiles.java
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestFiles.manifest
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import com.android.tools.lint.checks.infrastructure.TestMode
import org.junit.Test
import timber.lint.WrongTimberUsageDetector.Companion.issues

class WrongTimberUsageDetectorTest {
  private val TIMBER_STUB =
    kotlin(
      """
      |package timber.log
      |class Timber private constructor() {
      |  private companion object {
      |    @JvmStatic fun d(message: String?, vararg args: Any?) {}
      |    @JvmStatic fun d(t: Throwable?, message: String?, vararg args: Any?) {}
      |    @JvmStatic fun tag(tag: String) = Tree()
      |  }
      |  open class Tree {
      |    open fun d(message: String?, vararg args: Any?) {}
      |    open fun d(t: Throwable?, message: String?, vararg args: Any?) {}
      |  }
      |}
      """
        .trimMargin()
    )

  @Test
  fun usingAndroidLogWithTwoArguments() {
    lint()
      .files(
        java(
          """
          |package foo;
          |import android.util.Log;
          |public class Example {
          |  public void log() {
          |    Log.d("TAG", "msg");
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import android.util.Log
          |class Example {
          |  fun log() {
          |    Log.d("TAG", "msg")
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expect(
        """
        |src/foo/Example.java:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    Log.d("TAG", "msg");
        |    ~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    Log.d("TAG", "msg")
        |    ~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 5: Replace with Timber.tag("TAG").d("msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg");
        |+     Timber.tag("TAG").d("msg");
        |Fix for src/foo/Example.java line 5: Replace with Timber.d("msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg");
        |+     Timber.d("msg");
        |Fix for src/foo/Example.kt line 5: Replace with Timber.tag("TAG").d("msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg")
        |+     Timber.tag("TAG").d("msg")
        |Fix for src/foo/Example.kt line 5: Replace with Timber.d("msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg")
        |+     Timber.d("msg")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun usingAndroidLogWithThreeArguments() {
    lint()
      .files(
        java(
          """
          |package foo;
          |import android.util.Log;
          |public class Example {
          |  public void log() {
          |    Log.d("TAG", "msg", new Exception());
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import android.util.Log
          |class Example {
          |  fun log() {
          |    Log.d("TAG", "msg", Exception())
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expect(
        """
        |src/foo/Example.java:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    Log.d("TAG", "msg", new Exception());
        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:5: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    Log.d("TAG", "msg", Exception())
        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 5: Replace with Timber.tag("TAG").d(new Exception(), "msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg", new Exception());
        |+     Timber.tag("TAG").d(new Exception(), "msg");
        |Fix for src/foo/Example.java line 5: Replace with Timber.d(new Exception(), "msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg", new Exception());
        |+     Timber.d(new Exception(), "msg");
        |Fix for src/foo/Example.kt line 5: Replace with Timber.tag("TAG").d(Exception(), "msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg", Exception())
        |+     Timber.tag("TAG").d(Exception(), "msg")
        |Fix for src/foo/Example.kt line 5: Replace with Timber.d(Exception(), "msg"):
        |@@ -5 +5
        |-     Log.d("TAG", "msg", Exception())
        |+     Timber.d(Exception(), "msg")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun usingFullyQualifiedAndroidLogWithTwoArguments() {
    lint()
      .files(
        java(
          """
          |package foo;
          |public class Example {
          |  public void log() {
          |    android.util.Log.d("TAG", "msg");
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |class Example {
          |  fun log() {
          |    android.util.Log.d("TAG", "msg")
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expect(
        """
        |src/foo/Example.java:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    android.util.Log.d("TAG", "msg");
        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    android.util.Log.d("TAG", "msg")
        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 4: Replace with Timber.tag("TAG").d("msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg");
        |+     Timber.tag("TAG").d("msg");
        |Fix for src/foo/Example.java line 4: Replace with Timber.d("msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg");
        |+     Timber.d("msg");
        |Fix for src/foo/Example.kt line 4: Replace with Timber.tag("TAG").d("msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg")
        |+     Timber.tag("TAG").d("msg")
        |Fix for src/foo/Example.kt line 4: Replace with Timber.d("msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg")
        |+     Timber.d("msg")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun usingFullyQualifiedAndroidLogWithThreeArguments() {
    lint()
      .files(
        java(
          """
          |package foo;
          |public class Example {
          |  public void log() {
          |    android.util.Log.d("TAG", "msg", new Exception());
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |class Example {
          |  fun log() {
          |    android.util.Log.d("TAG", "msg", Exception())
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expect(
        """
        |src/foo/Example.java:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    android.util.Log.d("TAG", "msg", new Exception());
        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:4: Warning: Using 'Log' instead of 'Timber' [LogNotTimber]
        |    android.util.Log.d("TAG", "msg", Exception())
        |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 4: Replace with Timber.tag("TAG").d(new Exception(), "msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg", new Exception());
        |+     Timber.tag("TAG").d(new Exception(), "msg");
        |Fix for src/foo/Example.java line 4: Replace with Timber.d(new Exception(), "msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg", new Exception());
        |+     Timber.d(new Exception(), "msg");
        |Fix for src/foo/Example.kt line 4: Replace with Timber.tag("TAG").d(Exception(), "msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg", Exception())
        |+     Timber.tag("TAG").d(Exception(), "msg")
        |Fix for src/foo/Example.kt line 4: Replace with Timber.d(Exception(), "msg"):
        |@@ -4 +4
        |-     android.util.Log.d("TAG", "msg", Exception())
        |+     Timber.d(Exception(), "msg")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun innerStringFormat() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |     Timber.d(String.format("%s", "arg1"));
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |class Example {
          |  fun log() {
          |     Timber.d(String.format("%s", "arg1"))
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .skipTestModes(TestMode.WHITESPACE)
      .run()
      .expect(
        """
        |src/foo/Example.java:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |     Timber.d(String.format("%s", "arg1"));
        |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |     Timber.d(String.format("%s", "arg1"))
        |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 5: Remove String.format(...):
        |@@ -5 +5
        |-      Timber.d(String.format("%s", "arg1"));
        |+      Timber.d("%s", "arg1");
        |Fix for src/foo/Example.kt line 5: Remove String.format(...):
        |@@ -5 +5
        |-      Timber.d(String.format("%s", "arg1"))
        |+      Timber.d("%s", "arg1")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun innerStringFormatWithStaticImport() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |import static java.lang.String.format;
          |public class Example {
          |  public void log() {
          |     Timber.d(format("%s", "arg1"));
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |import java.lang.String.format
          |class Example {
          |  fun log() {
          |     Timber.d(format("%s", "arg1"))
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .skipTestModes(TestMode.PARENTHESIZED, TestMode.WHITESPACE)
      .run()
      .expect(
        """
        |src/foo/Example.java:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |     Timber.d(format("%s", "arg1"));
        |              ~~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |     Timber.d(format("%s", "arg1"))
        |              ~~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 6: Remove String.format(...):
        |@@ -6 +6
        |-      Timber.d(format("%s", "arg1"));
        |+      Timber.d("%s", "arg1");
        |Fix for src/foo/Example.kt line 6: Remove String.format(...):
        |@@ -6 +6
        |-      Timber.d(format("%s", "arg1"))
        |+      Timber.d("%s", "arg1")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun innerStringFormatInNestedMethods() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |     Timber.d(id(String.format("%s", "arg1")));
          |  }
          |  private String id(String s) { return s; }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |class Example {
          |  fun log() {
          |     Timber.d(id(String.format("%s", "arg1")))
          |  }
          |  private fun id(s: String): String { return s }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expect(
        """
        |src/foo/Example.java:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |     Timber.d(id(String.format("%s", "arg1")));
        |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:5: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |     Timber.d(id(String.format("%s", "arg1")))
        |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
  }

  @Test
  fun innerStringFormatInNestedAssignment() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |    String msg = null;
          |    Timber.d(msg = String.format("msg"));
          |  }
          |}
          """
            .trimMargin()
        ),
        // no kotlin equivalent, since nested assignments do not exist
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expect(
        """
        |src/foo/Example.java:6: Warning: Using 'String#format' inside of 'Timber' [StringFormatInTimber]
        |    Timber.d(msg = String.format("msg"));
        |                   ~~~~~~~~~~~~~~~~~~~~
        |0 errors, 1 warnings
        """
          .trimMargin()
      )
  }

  @Test
  fun validStringFormatInCodeBlock() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |public class Example {
          |  public void log() {
          |    for(;;) {
          |      String name = String.format("msg");
          |    }
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |class Example {
          |  fun log() {
          |    while(true) {
          |      val name = String.format("msg")
          |    }
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expectClean()
  }

  @Test
  fun validStringFormatInConstructorCall() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |public class Example {
          |  public void log() {
          |    new Exception(String.format("msg"));
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |class Example {
          |  fun log() {
          |    Exception(String.format("msg"))
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expectClean()
  }

  @Test
  fun validStringFormatInStaticArray() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |public class Example {
          |  static String[] X = { String.format("%s", 100) };
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |class Example {
          |  companion object {
          |    val X = arrayOf(String.format("%s", 100))
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expectClean()
  }

  @Test
  fun validStringFormatExtracted() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |    String message = String.format("%s", "foo");
          |    Timber.d(message);
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |class Example {
          |  fun log() {
          |    val message = String.format("%s", "foo")
          |    Timber.d(message)
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expectClean()
  }

  @Test
  fun throwableNotAtBeginning() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |     Exception e = new Exception();
          |     Timber.d("%s", e);
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |class Example {
          |  fun log() {
          |     val e = Exception()
          |     Timber.d("%s", e)
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .skipTestModes(TestMode.WHITESPACE)
      .run()
      .expect(
        """
        |src/foo/Example.java:6: Warning: Throwable should be first argument [ThrowableNotAtBeginning]
        |     Timber.d("%s", e);
        |     ~~~~~~~~~~~~~~~~~
        |src/foo/Example.kt:6: Warning: Throwable should be first argument [ThrowableNotAtBeginning]
        |     Timber.d("%s", e)
        |     ~~~~~~~~~~~~~~~~~
        |0 errors, 2 warnings
        """
          .trimMargin()
      )
      .expectFixDiffs(
        """
        |Fix for src/foo/Example.java line 6: Replace with e, "%s":
        |@@ -6 +6
        |-      Timber.d("%s", e);
        |+      Timber.d(e, "%s");
        |Fix for src/foo/Example.kt line 6: Replace with e, "%s":
        |@@ -6 +6
        |-      Timber.d("%s", e)
        |+      Timber.d(e, "%s")
        |"""
          .trimMargin()
      )
  }

  @Test
  fun stringConcatenationBothLiterals() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |     Timber.d("foo" + "bar");
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |class Example {
          |  fun log() {
          |     Timber.d("foo" + "bar")
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes(true)
      .issues(*issues)
      .run()
      .expectClean()
  }

  @Test
  fun stringConcatenationLeftLiteral() {
    lint()
      .files(
        TIMBER_STUB,
        java(
          """
          |package foo;
          |import timber.log.Timber;
          |public class Example {
          |  public void log() {
          |     String foo = "foo";
          |     Timber.d(foo + "bar");
          |  }
          |}
          """
            .trimMargin()
        ),
        kotlin(
          """
          |package foo
          |import timber.log.Timber
          |class Example {
          |  fun log() {
          |     val foo = "foo"
          |     Timber.d("${"$"}{foo}bar")
          |  }
          |}
          """
            .trimMargin()
        ),
      )
      .allowClassNameClashes
Download .txt
gitextract_ztbbr8oj/

├── .editorconfig
├── .github/
│   ├── renovate.json5
│   └── workflows/
│       ├── .java-version
│       ├── build.yaml
│       └── release.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── RELEASING.md
├── build.gradle
├── gradle/
│   ├── libs.versions.toml
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── timber/
│   ├── api/
│   │   └── timber.api
│   ├── build.gradle
│   ├── consumer-keep-rules.pro
│   ├── gradle.properties
│   └── src/
│       ├── androidMain/
│       │   └── kotlin/
│       │       └── timber/
│       │           └── log/
│       │               └── Timber.kt
│       ├── commonMain/
│       │   └── kotlin/
│       │       └── timber/
│       │           └── log/
│       │               └── Timber.kt
│       └── test/
│           └── java/
│               └── timber/
│                   └── log/
│                       ├── TimberJavaTest.java
│                       └── TimberTest.kt
├── timber-lint/
│   ├── build.gradle
│   ├── gradle.properties
│   ├── lint-baseline.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── timber/
│       │           └── lint/
│       │               ├── TimberIssueRegistry.kt
│       │               └── WrongTimberUsageDetector.kt
│       └── test/
│           └── java/
│               └── timber/
│                   └── lint/
│                       └── WrongTimberUsageDetectorTest.kt
└── timber-sample/
    ├── build.gradle
    └── src/
        └── main/
            ├── AndroidManifest.xml
            ├── java/
            │   └── com/
            │       └── example/
            │           └── timber/
            │               ├── ExampleApp.java
            │               ├── FakeCrashLibrary.java
            │               └── ui/
            │                   ├── DemoActivity.java
            │                   ├── JavaLintActivity.java
            │                   └── KotlinLintActivity.kt
            └── res/
                ├── layout/
                │   └── demo_activity.xml
                └── values/
                    └── strings.xml
Download .txt
SYMBOL INDEX (18 symbols across 5 files)

FILE: timber-sample/src/main/java/com/example/timber/ExampleApp.java
  class ExampleApp (line 12) | public class ExampleApp extends Application {
    method onCreate (line 13) | @Override public void onCreate() {
    class CrashReportingTree (line 24) | private static class CrashReportingTree extends Timber.Tree {
      method log (line 25) | @Override protected void log(int priority, String tag, @NonNull Stri...

FILE: timber-sample/src/main/java/com/example/timber/FakeCrashLibrary.java
  class FakeCrashLibrary (line 4) | public final class FakeCrashLibrary {
    method log (line 5) | public static void log(int priority, String tag, String message) {
    method logWarning (line 9) | public static void logWarning(Throwable t) {
    method logError (line 13) | public static void logError(Throwable t) {
    method FakeCrashLibrary (line 17) | private FakeCrashLibrary() {

FILE: timber-sample/src/main/java/com/example/timber/ui/DemoActivity.java
  class DemoActivity (line 15) | public class DemoActivity extends Activity implements View.OnClickListen...
    method onCreate (line 16) | @Override protected void onCreate(Bundle savedInstanceState) {
    method onClick (line 29) | @Override public void onClick(View v) {

FILE: timber-sample/src/main/java/com/example/timber/ui/JavaLintActivity.java
  class JavaLintActivity (line 12) | @SuppressLint("Registered") //
    method onCreate (line 20) | @SuppressLint({
    method getString (line 75) | private String getString() {

FILE: timber/src/test/java/timber/log/TimberJavaTest.java
  class TimberJavaTest (line 7) | public class TimberJavaTest {
    method nullTree (line 8) | @SuppressWarnings("ConstantConditions")
    method nullTreeArray (line 17) | @SuppressWarnings("ConstantConditions")
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (250K chars).
[
  {
    "path": ".editorconfig",
    "chars": 242,
    "preview": "root = true\n\n[*]\ninsert_final_newline=true\nend_of_line=lf\ncharset=utf-8\nindent_size=2\ntrim_trailing_whitespace=true\n\n[*."
  },
  {
    "path": ".github/renovate.json5",
    "chars": 715,
    "preview": "{\n\t$schema: 'https://docs.renovatebot.com/renovate-schema.json',\n\textends: [\n\t\t'config:recommended',\n\t],\n\tpackageRules: "
  },
  {
    "path": ".github/workflows/.java-version",
    "chars": 3,
    "preview": "25\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "chars": 1387,
    "preview": "name: build\n\non:\n  pull_request: {}\n  workflow_dispatch: {}\n  push:\n    branches:\n      - 'trunk'\n    tags-ignore:\n     "
  },
  {
    "path": ".github/workflows/release.yaml",
    "chars": 1448,
    "preview": "name: release\n\non:\n  push:\n    tags:\n      - '**'\n\nenv:\n  GRADLE_OPTS: \"-Dorg.gradle.daemon=false -Dorg.gradle.vfs.watch"
  },
  {
    "path": ".gitignore",
    "chars": 158,
    "preview": ".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\nj"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 10183,
    "preview": "# 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 so"
  },
  {
    "path": "LICENSE.txt",
    "chars": 11358,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 5241,
    "preview": "![Timber](logo.png)\n\nThis is a logger with a small, extensible API which provides utility on top of Android's normal\n`Lo"
  },
  {
    "path": "RELEASING.md",
    "chars": 1226,
    "preview": "# Releasing\n\n1. Update the `VERSION_NAME` in `gradle.properties` to the release version.\n\n2. Update the `CHANGELOG.md`:\n"
  },
  {
    "path": "build.gradle",
    "chars": 787,
    "preview": "buildscript {\n  repositories {\n    mavenCentral()\n    google()\n    gradlePluginPortal()\n  }\n\n  dependencies {\n    classp"
  },
  {
    "path": "gradle/libs.versions.toml",
    "chars": 1509,
    "preview": "[versions]\nagp = \"9.1.0\"\nandroidTools = \"32.1.0\" # Update this values in sync with agp.\nkotlin = \"2.3.20\"\nautoService = "
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 252,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 763,
    "preview": "GROUP=com.jakewharton.timber\n\n# HEY! If you change the major version here be sure to update release.yaml doc target fold"
  },
  {
    "path": "gradlew",
    "chars": 8631,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\")"
  },
  {
    "path": "gradlew.bat",
    "chars": 2896,
    "preview": "@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 (th"
  },
  {
    "path": "settings.gradle",
    "chars": 100,
    "preview": "include ':timber'\ninclude ':timber-lint'\ninclude ':timber-sample'\n\nrootProject.name = 'timber-root'\n"
  },
  {
    "path": "timber/api/timber.api",
    "chars": 5484,
    "preview": "public final class timber/log/Timber {\n\tpublic static final field Forest Ltimber/log/Timber$Forest;\n\tpublic static fun a"
  },
  {
    "path": "timber/build.gradle",
    "chars": 1310,
    "preview": "import tapmoc.Severity\n\napply plugin: 'org.jetbrains.kotlin.multiplatform'\napply plugin: 'com.android.kotlin.multiplatfo"
  },
  {
    "path": "timber/consumer-keep-rules.pro",
    "chars": 39,
    "preview": "-dontwarn org.jetbrains.annotations.**\n"
  },
  {
    "path": "timber/gradle.properties",
    "chars": 39,
    "preview": "POM_ARTIFACT_ID=timber\nPOM_NAME=Timber\n"
  },
  {
    "path": "timber/src/androidMain/kotlin/timber/log/Timber.kt",
    "chars": 15505,
    "preview": "package timber.log\n\nimport android.os.Build\nimport android.util.Log\nimport java.io.PrintWriter\nimport java.io.StringWrit"
  },
  {
    "path": "timber/src/commonMain/kotlin/timber/log/Timber.kt",
    "chars": 3776,
    "preview": "package timber.log\n\n/** Logging for lazy people. */\nexpect class Timber private constructor() {\n\n  /** A facade for hand"
  },
  {
    "path": "timber/src/test/java/timber/log/TimberJavaTest.java",
    "chars": 638,
    "preview": "package timber.log;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.fail;\n\npublic class TimberJavaTest {\n  @Supp"
  },
  {
    "path": "timber/src/test/java/timber/log/TimberTest.kt",
    "chars": 19478,
    "preview": "package timber.log\n\nimport android.os.Build\nimport android.util.Log\nimport assertk.assertFailure\nimport assertk.assertTh"
  },
  {
    "path": "timber-lint/build.gradle",
    "chars": 499,
    "preview": "apply plugin: 'org.jetbrains.kotlin.jvm'\napply plugin: 'org.jetbrains.kotlin.kapt'\napply plugin: 'com.android.lint'\nappl"
  },
  {
    "path": "timber-lint/gradle.properties",
    "chars": 194,
    "preview": "# needed so that :timber:prepareLintJarForPublish can succeed\n# Remove when the bug described in https://issuetracker.go"
  },
  {
    "path": "timber-lint/lint-baseline.xml",
    "chars": 48360,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<issues format=\"6\" by=\"lint 8.12.0\" type=\"baseline\" client=\"gradle\" dependencies="
  },
  {
    "path": "timber-lint/src/main/java/timber/lint/TimberIssueRegistry.kt",
    "chars": 893,
    "preview": "package timber.lint\n\nimport com.android.tools.lint.client.api.IssueRegistry\nimport com.android.tools.lint.client.api.Ven"
  },
  {
    "path": "timber-lint/src/main/java/timber/lint/WrongTimberUsageDetector.kt",
    "chars": 31606,
    "preview": "package timber.lint\n\nimport com.android.tools.lint.checks.StringFormatDetector\nimport com.android.tools.lint.client.api."
  },
  {
    "path": "timber-lint/src/test/java/timber/lint/WrongTimberUsageDetectorTest.kt",
    "chars": 52113,
    "preview": "package timber.lint\n\nimport com.android.tools.lint.checks.infrastructure.TestFiles.java\nimport com.android.tools.lint.ch"
  },
  {
    "path": "timber-sample/build.gradle",
    "chars": 578,
    "preview": "apply plugin: 'com.android.application'\n\nandroid {\n  namespace 'com.example.timber'\n  compileSdkVersion libs.versions.co"
  },
  {
    "path": "timber-sample/src/main/AndroidManifest.xml",
    "chars": 725,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:t"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ExampleApp.java",
    "chars": 1025,
    "preview": "package com.example.timber;\n\nimport static timber.log.Timber.DebugTree;\n\nimport android.app.Application;\nimport android."
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/FakeCrashLibrary.java",
    "chars": 490,
    "preview": "package com.example.timber;\n\n/** Not a real crash reporting library! */\npublic final class FakeCrashLibrary {\n  public s"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ui/DemoActivity.java",
    "chars": 1068,
    "preview": "package com.example.timber.ui;\n\nimport static android.widget.Toast.LENGTH_SHORT;\n\nimport android.app.Activity;\nimport an"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ui/JavaLintActivity.java",
    "chars": 2135,
    "preview": "package com.example.timber.ui;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.os.B"
  },
  {
    "path": "timber-sample/src/main/java/com/example/timber/ui/KotlinLintActivity.kt",
    "chars": 1984,
    "preview": "package com.example.timber.ui\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.os.Bund"
  },
  {
    "path": "timber-sample/src/main/res/layout/demo_activity.xml",
    "chars": 852,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    and"
  },
  {
    "path": "timber-sample/src/main/res/values/strings.xml",
    "chars": 212,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources>\n  <string name=\"app_name\">Timber</string>\n\n  <string name=\"hello\">He"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the JakeWharton/timber GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (230.4 KB), approximately 58.1k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!