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
================================================

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=""format()" looks like a call; surround with backtics in string to display as symbol, e.g. \`format()\`"
errorLine1=" explanation = "Since Timber handles String.format automatically, you may not use String#format().","
errorLine2=" ~~~~~~~~">
<location
file="src/main/java/timber/lint/WrongTimberUsageDetector.kt"
line="760"
column="95"/>
</issue>
<issue
id="LintImplTextFormat"
message=""format()" looks like a call; surround with backtics in string to display as symbol, e.g. \`format()\`"
errorLine1=" explanation = "Since Timber handles String#format() automatically, use this instead of String concatenation.","
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("${"$"}{foo}bar")"
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("foo${"$"}bar")"
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("${"$"}{foo}${"$"}bar")"
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) "Hello, ${"$"}{s}" else "Bye")"
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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" |}""".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=" """.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=" """.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=" """.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
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
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": "\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.