Repository: slackhq/slack-lints Branch: main Commit: f58cfb5fc403 Files: 160 Total size: 940.0 KB Directory structure: gitextract_dsk5zpit/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── maintainers_guide.md │ └── workflows/ │ ├── ci.yml │ ├── increment_version.sh │ ├── renovate.yml │ └── scriptUtil.sh ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── release.sh ├── renovate.json ├── settings.gradle.kts ├── slack-lint-annotations/ │ ├── build.gradle.kts │ ├── gradle.properties │ └── src/ │ └── main/ │ └── kotlin/ │ └── slack/ │ └── lint/ │ └── annotations/ │ ├── AllowUnitResult.kt │ ├── DoNotMock.kt │ ├── JavaOnly.kt │ ├── KotlinOnly.kt │ ├── MustUseNamedParams.kt │ └── RestrictCallsTo.kt ├── slack-lint-checks/ │ ├── build.gradle.kts │ ├── gradle.properties │ ├── lint-baseline.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── slack/ │ │ └── lint/ │ │ ├── AlwaysNullReadOnlyVariableDetector.kt │ │ ├── AnnotatedClassOrMethodUsageDetector.kt │ │ ├── ArgInFormattedQuantityStringResDetector.kt │ │ ├── CircuitScreenDataClassDetector.kt │ │ ├── DaggerIssuesDetector.kt │ │ ├── DeprecatedAnnotationDetector.kt │ │ ├── DeprecatedSqlUsageDetector.kt │ │ ├── DoNotCallProvidersDetector.kt │ │ ├── ExceptionMessageDetector.kt │ │ ├── FragmentDaggerFieldInjectionDetector.kt │ │ ├── GuavaPreconditionsDetector.kt │ │ ├── InjectInJavaDetector.kt │ │ ├── JavaOnlyDetector.kt │ │ ├── JsonInflaterMoshiCompatibilityDetector.kt │ │ ├── MainScopeUsageDetector.kt │ │ ├── MoshiUsageDetector.kt │ │ ├── MustUseNamedParamsDetector.kt │ │ ├── NonKotlinPairDetector.kt │ │ ├── NotNullOperatorDetector.kt │ │ ├── NullableConcurrentHashMapDetector.kt │ │ ├── RawDispatchersUsageDetector.kt │ │ ├── RedactedUsageDetector.kt │ │ ├── RestrictCallsToDetector.kt │ │ ├── SerializableDetector.kt │ │ ├── SlackIssueRegistry.kt │ │ ├── TestParameterSiteTargetDetector.kt │ │ ├── ViewContextDetector.kt │ │ ├── denylistedapis/ │ │ │ └── DenyListedApiDetector.kt │ │ ├── eithernet/ │ │ │ └── DoNotExposeEitherNetInRepositoriesDetector.kt │ │ ├── inclusive/ │ │ │ ├── InclusiveNamingChecker.kt │ │ │ ├── InclusiveNamingResourceScanner.kt │ │ │ └── InclusiveNamingSourceCodeScanner.kt │ │ ├── mocking/ │ │ │ ├── AnyMockDetector.kt │ │ │ ├── AutoValueMockDetector.kt │ │ │ ├── DataClassMockDetector.kt │ │ │ ├── DoNotMockMockDetector.kt │ │ │ ├── ErrorProneDoNotMockDetector.kt │ │ │ ├── MockDetector.kt │ │ │ ├── ObjectClassMockDetector.kt │ │ │ ├── PlatformTypeMockDetector.kt │ │ │ ├── RecordClassMockDetector.kt │ │ │ ├── SealedClassMockDetector.kt │ │ │ └── ValueClassMockDetector.kt │ │ ├── moshi/ │ │ │ └── MoshiLintUtil.kt │ │ ├── parcel/ │ │ │ └── ParcelizeFunctionPropertyDetector.kt │ │ ├── resources/ │ │ │ ├── FullyQualifiedResourceDetector.kt │ │ │ ├── ImportAliasesLoader.kt │ │ │ ├── MissingResourceImportAliasDetector.kt │ │ │ ├── WrongResourceImportAliasDetector.kt │ │ │ └── model/ │ │ │ └── RootIssueData.kt │ │ ├── retrofit/ │ │ │ └── RetrofitUsageDetector.kt │ │ ├── rx/ │ │ │ ├── RxObservableEmitDetector.kt │ │ │ └── RxSubscribeOnMainDetector.kt │ │ ├── text/ │ │ │ └── SpanMarkPointMissingMaskDetector.kt │ │ ├── ui/ │ │ │ ├── DoNotCallViewToString.kt │ │ │ └── ItemDecorationViewBindingDetector.kt │ │ └── util/ │ │ ├── BooleanLintOption.kt │ │ ├── LintOption.kt │ │ ├── LintUtils.kt │ │ ├── MetadataJavaEvaluator.kt │ │ ├── Names.kt │ │ ├── OptionLoadingDetector.kt │ │ ├── Priorities.kt │ │ └── StringSetLintOption.kt │ └── test/ │ ├── java/ │ │ └── slack/ │ │ └── lint/ │ │ ├── AlwaysNullReadOnlyVariableDetectorTest.kt │ │ ├── ArgInFormattedQuantityStringResDetectorTest.kt │ │ ├── BaseSlackLintTest.kt │ │ ├── CircuitScreenDataClassDetectorTest.kt │ │ ├── DaggerIssuesDetectorTest.kt │ │ ├── DeprecatedAnnotationDetectorTest.kt │ │ ├── DeprecatedSqlUsageDetectorTest.kt │ │ ├── DoNotCallProvidersDetectorTest.kt │ │ ├── ExceptionMessageDetectorTest.kt │ │ ├── FragmentDaggerFieldInjectionDetectorTest.kt │ │ ├── GuavaPreconditionsDetectorTest.kt │ │ ├── InjectInJavaDetectorTest.kt │ │ ├── JavaOnlyDetectorTest.kt │ │ ├── JsonInflaterMoshiCompatibilityDetectorTest.kt │ │ ├── LintKotlinVersionCheckTest.kt │ │ ├── MainScopeUsageDetectorTest.kt │ │ ├── MoshiEnumUsageDetectorTest.kt │ │ ├── MoshiUsageDetectorTest.kt │ │ ├── MustUseNamedParamsDetectorTest.kt │ │ ├── NonKotlinPairDetectorTest.kt │ │ ├── NotNullOperatorDetectorTest.kt │ │ ├── NullableConcurrentHashMapDetectorTest.kt │ │ ├── RawDispatchersUsageDetectorTest.kt │ │ ├── RedactedUsageDetectorTest.kt │ │ ├── RestrictCallsToDetectorTest.kt │ │ ├── SerializableDetectorTest.kt │ │ ├── TestParameterSiteTargetDetectorTest.kt │ │ ├── ViewContextDetectorTest.kt │ │ ├── denylistedapis/ │ │ │ └── DenyListedApiDetectorTest.kt │ │ ├── eithernet/ │ │ │ └── DoNotExposeEitherNetInRepositoriesDetectorTest.kt │ │ ├── inclusive/ │ │ │ └── InclusiveNamingDetectorTest.kt │ │ ├── mocking/ │ │ │ ├── AutoValueMockDetectorTest.kt │ │ │ ├── DataClassMockDetectorTest.kt │ │ │ ├── DoNotMockMockDetectorTest.kt │ │ │ ├── DoNotMockUsageDetectorTest.kt │ │ │ ├── MockDetectorOptionsTest.kt │ │ │ ├── MockFileStubs.kt │ │ │ ├── MockReportTest.kt │ │ │ ├── ObjectClassMockDetectorTest.kt │ │ │ ├── PlatformTypeMockDetectorTest.kt │ │ │ ├── RecordClassMockDetectorTest.kt │ │ │ ├── SealedClassMockDetectorTest.kt │ │ │ └── ValueClassMockDetectorTest.kt │ │ ├── parcel/ │ │ │ └── ParcelizeFunctionPropertyDetectorTest.kt │ │ ├── resources/ │ │ │ ├── FullyQualifiedResourceDetectorTest.kt │ │ │ ├── MissingResourceImportAliasDetectorTest.kt │ │ │ └── WrongResourceImportAliasDetectorTest.kt │ │ ├── retrofit/ │ │ │ ├── RetrofitJarLoader.kt │ │ │ └── RetrofitUsageDetectorTest.kt │ │ ├── rx/ │ │ │ ├── RxJavaJarLoader.kt │ │ │ ├── RxObservableEmitDetectorTest.kt │ │ │ └── RxSubscribeOnMainDetectorTest.kt │ │ ├── text/ │ │ │ └── SpanMarkPointMissingMaskDetectorTest.kt │ │ └── util/ │ │ └── LintUtilsTest.kt │ └── resources/ │ ├── com/ │ │ └── slack/ │ │ └── lint/ │ │ └── data/ │ │ └── testStubs/ │ │ ├── ViewContextDetectorTestContentProvider.java │ │ ├── ViewContextDetectorTestCustomViewInternalCaller.java │ │ ├── ViewContextDetectorTestExternalCallerOnCustomView.java │ │ └── ViewContextDetectorTestExternalCallerOnView.java │ ├── retrofit-3.0.0.jar │ └── rxjava-3.1.0.jar └── spotless/ └── spotless.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Introduction Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand. Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic. This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members. For more information on our code of conduct, please visit [https://slackhq.github.io/code-of-conduct](https://slackhq.github.io/code-of-conduct) ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributors Guide Note that this project is considered READ-ONLY. You are welcome to discuss or ask questions in the discussions section of the repo, but we do not normally accept external contributions without prior discussion. ## Development Check out this repo with Android Studio or IntelliJ. It's a standard gradle project and conventional to check out. The primary project is `slack-lint`. Kotlin should be used for more idiomatic use with lint APIs. ## Setup Be sure your devel environment has `ANDROID_HOME` defined or you'll have trouble running tests that require the Android SDK. If you've added it and still seeing the error about not having it defined while running tests, try closing and re-opening Android Studio. ## Lint Documentation [The Android Lint API Guide](https://googlesamples.github.io/android-custom-lint-rules/book.html) provides an excellent overview of lint's purpose, how it works, and how to author custom checks. ## Lint Guidelines - Limited scopes. Remember this will run in a slow build step or during the IDE, performance matters! - If your check only matters for java or kotlin, only run on appropriate files - Use the smallest necessary scope. Avoid tree walking through the AST if it can be avoided, there are usually more appropriate hooks. - Use `UElementHandler` (via overriding `createUastHandler()`) rather than overriding `Detector` callback methods. `Detector` callback methods tend only to be useful for tricky scenarios, like annotated elements. For basic `UElement` types it's best to just use `UElementHandler` as it affords a standard API and is easy to conditionally avoid nested parsing. - For testing, prefer writing source stubs directly in the test rather than extract individual files in `resources` for stubs. Stubs in resources add friction for source glancing and tedious to maintain, and should only be used for extremely complex source files. - Use our `implementation<*Detector>()` helper functions for wiring your `Issue` information. This is important because it will help ensure your check works in both command line and in the IDE. ## Maintainers There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md). ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Summary Describe the goal of this PR. Mention any related Issue numbers. Please note that we do not normally accept new externally-contributed PRs to this repo unless it's something Slack-relevant and came up in prior discussion ### Requirements (place an `x` in each `[ ]`) * [ ] I've read and understood the [Contributing Guidelines](https://github.com/{project_slug}/blob/master/.github/contributing.md) and have done my best effort to follow them. * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). > The following point can be removed after setting up CI (such as Travis) with coverage reports (such as Codecov) * [ ] I've written tests to cover the new code and functionality included in this PR. > The following point can be removed after setting up a CLA reporting tool such as cla-assistant.io * [ ] I've read, agree to, and signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/{project_slug}). ================================================ FILE: .github/maintainers_guide.md ================================================ # Maintainers Guide This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain this project. If you use this package within your own software as is but don't plan on modifying it, this guide is **not** for you. ## Tools (optional) > Are there any build tools, dependencies, or other programs someone maintaining this project > needs to be familiar with? ## Tasks ### Testing > How do you run the tests? ### Generating Documentation (optional) > If the documentation is generated from source, how does someone run the generation? > Are the docs published on a website (GitHub Pages)? ### Releasing > A description of the process to make a release for this project. Do not share any secrets here. ## Workflow ### Versioning and Tags > Does this project use semver? What does the numbering system look like? Are releases tagged in git? ### Branches > Describe any specific branching workflow. For example: > `master` is where active development occurs. > Long running branches named feature branches are occasionally created for collaboration on a feature that has a large scope (because everyone cannot push commits to another person's open Pull Request) > At some point in the future after a major version increment, there may be maintenance branches > for older major versions. ### Issue Management Labels are used to run issues through an organized workflow. Here are the basic definitions: * `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been documented and the issue has been reproduced. * `enhancement`: A feature request for something this package might not already do. * `docs`: An issue that is purely about documentation work. * `tests`: An issue that is purely about testing work. * `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information. * `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues. * `question`: An issue that is like a support request because the user's usage was not correct. * `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number. * `security`: An issue that has special consideration for security reasons. * `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known. * `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number. > You may want to add more labels for subsystems of your project, depending on how complex it is. **Triage** is the process of taking new issues that aren't yet "seen" and marking them with a basic level of information with labels. An issue should have **one** of the following labels applied: `bug`, `enhancement`, `question`, `needs feedback`, `docs`, `tests`, or `discussion`. Issues are closed when a resolution has been reached. If for any reason a closed issue seems relevant once again, reopening is great and better than creating a duplicate issue. ## Everything else When in doubt, find the other maintainers and ask. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: # Only run push on main push: branches: - main paths-ignore: - '**/*.md' # Always run on PRs pull_request: branches: [ main ] merge_group: concurrency: group: 'ci-${{ github.event.merge_group.head_ref || github.head_ref }}-${{ github.workflow }}' cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Install JDK uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: '23' - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - name: Build and run tests id: gradle run: ./gradlew check --quiet - uses: mikepenz/action-junit-report@v6 if: success() || failure() with: report_paths: '**/build/test-results/test/TEST-*.xml' - name: (Fail-only) Upload build reports if: failure() uses: actions/upload-artifact@v7 with: name: reports path: | **/build/reports/** - name: Publish snapshot (main branch only) if: github.repository == 'slackhq/slack-lint-checks' && github.ref == 'refs/heads/main' run: ./gradlew publish -PmavenCentralUsername=${{ secrets.SONATYPEUSERNAME }} -PmavenCentralPassword=${{ secrets.SONATYPEPASSWORD }} ================================================ FILE: .github/workflows/increment_version.sh ================================================ #!/usr/bin/env bash # Target project to update, required TARGET=$1 # Version to increment to, optional. If blank or not specified, will auto-increment. REQUESTED_VERSION=$2 source .github/workflows/scriptUtil.sh # Parse the current version, strip leading zeros, increment CURRENT_VERSION=$(getProperty 'VERSION_NAME' "$TARGET"/gradle.properties) # Export the coordinates while we're at it for later use in publish.yml # Group is always in our root dir GROUP=$(getProperty 'GROUP' gradle.properties) ARTIFACT=$(getProperty 'POM_ARTIFACT_ID' "$TARGET"/gradle.properties) STRIPPED_CURRENT=$(echo "$CURRENT_VERSION" | sed 's/^0*//') if [[ "$REQUESTED_VERSION" != "" ]]; then NEW_VERSION=$REQUESTED_VERSION else ((STRIPPED_CURRENT++)) NEW_VERSION=$(printf "%05d\n" $STRIPPED_CURRENT) fi sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" "$TARGET"/gradle.properties echo "current: $CURRENT_VERSION" echo "CURRENT_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV echo "new: $NEW_VERSION" echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV # We just use the artifact ID in the android repo for the coordinate echo "COORDINATES=$ARTIFACT" >> $GITHUB_ENV ================================================ FILE: .github/workflows/renovate.yml ================================================ name: Renovate on: schedule: - cron: "0 8 * * *" # 8am daily workflow_dispatch: jobs: renovate: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Self-hosted Renovate uses: renovatebot/github-action@v32.190.6 with: configurationFile: renovate.json token: ${{ secrets.SLACKHQ_MBR_GITHUB_TOKEN }} ================================================ FILE: .github/workflows/scriptUtil.sh ================================================ # Source this file to use its functions # Gets a property out of a .properties file # usage: getProperty $key $filename function getProperty() { grep "${1}" "$2" | cut -d'=' -f2 } ================================================ FILE: .gitignore ================================================ .gradle .kotlin/ local.properties .idea !/.idea/codeStyles !/.idea/inspectionProfiles !/.idea/icon.png *.iml *.so .DS_Store build captures version.properties env node_modules reports .cxx # IDE-generated dir out/ # We have one gitignore but project generation often generates extra ones. Ignore those */**/.gitignore ================================================ FILE: CHANGELOG.md ================================================ Changelog ========= **Unreleased** -------------- 0.11.1 ------ _2025-10-10_ - **Fix**: Allow sealed classes to be Moshi-compatible. - **Fix**: Allow primitives in collections to be Moshi-compatible. - **Fix**: Allow sealed interfaces to be Moshi-compatible. - **Fix**: Allow enums to be Moshi-compatible. - Update Kotlin to `2.2.10`. - Update plugin lint to `8.13.0`. - Update dependency gradle to `9.1.0`. - Update various build plugins and GitHub Actions. 0.11.0 ------ _2025-07-17_ - **New**: Add `CircuitScreenDataClassDetector` check to ensure that [Circuit](https://github.com/slackhq/circuit) `Screen` classes are data classes or data objects. - **Fix**: Enable the `JsonInflaterMoshiCompatibilityDetector` in slack-lint's lint registry. - **Fix**: _Actually_ fix "You must override visitCallExpression (and don't call super.visitCallExpression!)" error. - **Change**: Disable `AlwaysNullReadOnlyVariableDetector` for now as this was causing the issue above it seems and we haven't had a change to investigate further. - Build against lint `31.12.0-alpha09`. 0.10.1 ------ _2025-07-14_ - Fix "You must override visitCallExpression (and don't call super.visitCallExpression!)" error. 0.10.0 ------ _2025-06-30_ - **New:** Add `JsonInflaterMoshiCompatibilityDetector` check. This lint is only usable to slack's internal repo. - **New:** Add `RxObservableEmitDetector` to ensure that `rxObservable`/`rxFlowable` lambda expressions call `send`/`trySend`. - **New:** Add `AlwaysNullReadOnlyVariableDetector` to lint against read-only variables always initialized to null. - Allow `suspend` Retrofit functions to return `Unit` if annotated with `@AllowUnitResult`. - Update lint to `31.12.0-alpha07`. - Only test against K2 UAST now. - Test against Retrofit `3.0.0`. Special thanks to [@henni99](https://github.com/henni99) for contributing to this release! 0.9.0 ----- _2025-03-27_ - **New**: Add `TestParameterSiteTarget` check to protect against https://github.com/google/TestParameterInjector/issues/49. - **New**: Add `NullableConcurrentHashMap` check to protect against putting null keys or values into `ConcurrentHashMap`s. - Add mockito-kotlin mock/spy functions to default `DoNotMock` checks. - Add `java.util.Calendar` to `DenyListedApiDetector`. - Don't require `ExceptionMessage` lint in tests. - Update EitherNet checks to EitherNet 2.0. - Raise lint registry API version to `16` (`8.7.0-alpha04`). - Build against Kotlin `2.1.20`. - Build against lint `31.10.0-alpha03`. Special thanks to [@mformetal](https://github.com/mformetal) and [@jbduncan](https://github.com/jbduncan) for contributing to this release! 0.8.2 ----- _2024-10-14_ - **Enhancement**: Handle `@Multipart` and `@Part` annotations in Retrofit lints. 0.8.1 ----- _2024-10-03_ - Open-source `AvoidUsingNotNullOperator`, `InflationInItemDecoration`, and `DoNotCallViewToString` checks. 0.8.0 ----- _2024-10-02_ - **Enhancement**: Tweak explanation for default dispatcher use in rx<->flow interop. - **Enhancement**: Switch to stable kotlin-metadata artifact - **Fix**: Allow Dagger providers to be called from test sources. - Build against lint `8.8.0-alpha04`. - Update `api`/`minApi` to `16` (Lint 8.7.0+). - Build against Kotlin `2.0.20`. - Target Kotlin language version `1.9` in lint-checks (imposed by lint), `2.0` in lint-annotations. 0.7.3 ----- _2024-05-03_ - Fix `DoNotMockAnything` to use `setEnabledByDefault(false)`. 0.7.2 ----- _2024-05-02_ - Add new `DoNotMockAnything` check. This is disabled by default. This marks _any_ mock as a lint error. This is useful for enforcing a no-mocks policy in your codebase. - Update lint to `31.5.0-alpha07`. - Update to kotlin `1.9.23`. - [docs] Expand Mock option explanation for use with multiple issues. Special thanks to [@utwyko](https://github.com/utwyko) for contributing to this release! 0.7.1 ----- _2024-03-27_ - Add `MustUseNamedParamsDetector` to lint registry. - Update lint to `31.5.0-alpha02`. - Target Kotlin API/language version `1.9`. 0.7.0 ----- _2023-10-27_ - Lower lint API back to `31.3.0-alpha05` as newer versions targeted kotlin 1.9.20 betas without us realizing it. - Improve explanation for sealed class mock detector to mention that Mockito can't mock them at all in Java 17+. - Promote `PlatformTypeMockDetector` to error severity. - Make `DenyListedApi` entries more configurable. Initial change is that blocking APIs are now reported with the ID `DenyListedBlockingApi`. - Support multiple mock report modes for the `mock-report` option. Modes are `NONE`, `ERRORS`, and `ALL`. Default is `NONE`. Now the report file is `build/reports/mockdetector/mock-report.csv` and the second column is the severity. This allows reporting all mocks for extra analysis. 0.6.1 ----- _2023-10-09_ - **Enhancement**: Add `mock-report` option to `MockDetector`s to generate a report of all mocked types in a project. - Update to lint `31.3.0-alpha07`. 0.6.0 ----- _2023-09-28_ - **New**: Add `ExceptionMessage` check that ensures that calls to `check`, `checkNotNull`, `require`, and `requireNotNull` functions always include a message. - **Enhancement**: Add support for custom mock factories and mock annotations to `MockDetector`. - `mock-annotations` is a comma-separated list of mock annotations' fully-qualified names. Default is `org.mockito.Mock,org.mockito.Spy`. - `mock-factories` is a comma-separated list of mock factories (i.e. `org.mockito.Mockito#methodName`). Default is `org.mockito.Mockito#mock,org.mockito.Mockito#spy,slack.test.mockito.MockitoHelpers#mock,slack.test.mockito.MockitoHelpersKt#mock`. - Update lint to `31.3.0-alpha05`. Special thanks to [@SimonMarquis](https://github.com/SimonMarquis) for contributing to this release! 0.5.1 ----- _2023-09-09_ - **Fix**: Allow `@Provides` in companion objects of `@Module` classes. 0.5.0 ----- _2023-09-08_ - **New**: Add a bunch more checks around correct usage of Dagger `@Binds` and `@Provides` methods. - **Fix**: Remove `BindsCanBeExtensionFunction` lint as this is prohibited now in Dagger 2.48+. - Update to lint `31.3.0-alpha03`. - Update to Kotlin `1.9.10`. 0.4.0 ----- _2023-07-20_ - **New**: Denylist blocking RxJava 3 operators and coroutines' `runBlocking` in favor of `TestObserver` and `runTest`/Turbine. - **New**: Denylist coroutines' `runCatching`. - **New**: Denylist `java.util.Date`, `java.text.DateFormat`, and `java.text.SimpleDateFormat` in favor of `java.time.*`/`kotlin.time.*`/etc APIs. - **Enhancement**: Specifically report denylisted function name only in lint report, not the whole call expression. - **Enhancement**: Update kotlinx-metadata to `0.7.0` to better support Kotlin 2.0. - Update to lint `31.2.0-alpha13` (lint API `14`). 0.3.0 ----- _2023-05-31_ - **New**: Use kotlinx-metadata to parse `Metadata` annotations of `PsiCompiledElement` types to better handle Kotlin language features. Currently used in mock checks and Moshi checks. Please star this issue: https://issuetracker.google.com/issues/283654244. - **New**: Add DoNotMock check for `object` types. - **New**: Add DoNotMock check for `sealed` types. Subtypes should be used instead. - **New**: Add DoNotMock check for `record` types. Same motivation as data classes. - **New**: Add DoNotMock check for platform types (e.g. `java.*`, `kotlin.*`, `android.*`, their `*x.*` variants). Prefer real implementations or fakes instead. - This is a big change so this one is just a warning for now. - **Enhancement**: `MockDetector` revamp. All mock checks now run within the same detector to better utilize metadata catching. - **Enhancement**: Improve mock check location reporting. - **Enhancement**: Improve mock check messages to specify the erroring type. - **Enhancement**: Add `reason` properties to `@KotlinOnly`/`@JavaOnly` annotations. - **Enhancement**: Add more information to the `Vendor` details. - Raise min lint API to `14`. - Update kotlin to `1.8.21`. Updated language version to this too to match lint. - Update lint to `31.2.0-alpha06`. 0.2.3 ----- _2023-02-22_ - **New**: `ParcelizeFunctionProperty` check that errors when a `@Parcelize` class has a function property. 0.2.2 ----- _2023-02-09_ - **Removed**: Compose lints have been removed and published in a separate project: https://github.com/slackhq/compose-lints 0.2.1 ----- _2023-01-26_ - **Fix**: Improve and fix a number of explanation string formatting in the new compose lints. 0.2.0 ----- _2023-01-25_ - **New**: Ported most of the Twitter [compose-rules](https://github.com/twitter/compose-rules) checks to lint. We're packaging them in this project right now, but will likely publish them from a separate repo in the future. - Target lint-api `31.1.0-alpha01`. - Update to Kotlin API version `1.7`. Lint `8.1.0-alpha01` or later is now required. - Modernize various build infra (Kotlin `1.8.0`, JDK 19, Gradle 7.6). 0.1.1 ----- _2022-11-30_ - **Fix**: Fallback to file package name in `MissingResourceImportAliasDetector` if project package name is null. 0.1.0 ----- _2022-11-17_ * Initial release on maven central. ================================================ FILE: CODEOWNERS ================================================ # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. #ECCN:Open Source #GUSINFO:Open Source,Open Source Workflow ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 https://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 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. ================================================ FILE: README.md ================================================ slack-lints =========== This repository contains a collection of custom Android/Kotlin lint checks we use in our Android and Kotlin code bases at Slack. While we do publish artifacts to Maven Central, some of the lints checks may only really be relevant to Slack's codebase. We [develop these in the open](https://slack.engineering/developing-in-the-open/) to knowledge-share with the community. ## Installation Add the dependency to the `lintChecks` configuration. Note for non-android projects, you must apply the `com.android.lint` Gradle plugin to use this. [![Maven Central](https://img.shields.io/maven-central/v/com.slack.lint/slack-lint-checks.svg)](https://mvnrepository.com/artifact/com.slack.lint/slack-lint-checks) ```kotlin dependencies { lintChecks("com.slack.lint:slack-lint-checks:") } ``` ## Overview ### Do Not Mock The `slack.lint.mocking` package contains several detectors and utilities for detecting mocking of types that should not be mocked. This is similar to ErrorProne's `DoNotMockChecker` and acts as an enforcement layer to APIs and classes annotated with `@DoNotMock`. This also detects common types that should never be mocked, such as Kotlin `data` classes or AutoValue classes. ### Inclusivity In order to write more inclusive code, we have an `InclusiveNamingChecker` tool to check for a configurable list of non-inclusive names. ### Moshi `MoshiUsageDetector` contains a wealth of checks for common programmer errors when writing classes for use with [Moshi](https://github.com/square/moshi) and [MoshiX](https://github.com/ZacSweers/MoshiX). ### Misc * `JavaOnlyDetector` - detects use of Java-only APIs from Kotlin. Based on the original unreleased implementation in [uber/lint-checks](https://github.com/uber/lint-checks). * `DaggerKotlinIssuesDetector` - detects some known issues when using [Dagger](https://github.com/google/dagger) in Kotlin code. * `RetrofitUsageDetector` - detects some common issues when using [Retrofit](https://github.com/square/retrofit). * `DenyListedApi` – detects use of APIs that just shouldn't be used. * `MustUseNamedParams` – can be used on functions that should _always_ use named parameters. Useful for APIs that have a lot of parameters and/or may change their order and you want to keep changes source-compatible. * ...and a plethora of others! License -------- Copyright 2021 Slack Technologies, LLC 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: RELEASING.md ================================================ Releasing ========= 1. Update the `CHANGELOG.md` for the impending release. 2. Run `./release.sh `. 3. Publish the release on the repo's releases tab. ================================================ FILE: build.gradle.kts ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 import com.diffplug.gradle.spotless.KotlinExtension import com.diffplug.gradle.spotless.SpotlessExtension import com.vanniktech.maven.publish.MavenPublishBaseExtension import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.dokka.gradle.DokkaTaskPartial import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.spotless) apply false alias(libs.plugins.mavenPublish) apply false alias(libs.plugins.dokka) alias(libs.plugins.detekt) alias(libs.plugins.lint) apply false alias(libs.plugins.ksp) apply false } dokka { dokkaPublications.html { outputDirectory.set(rootDir.resolve("docs/api/0.x")) includes.from(project.layout.projectDirectory.file("README.md")) } } val ktfmtVersion = libs.versions.ktfmt.get() allprojects { apply(plugin = "com.diffplug.spotless") configure { format("misc") { target("*.md", ".gitignore") trimTrailingWhitespace() endWithNewline() } val externalSourceGlobs = arrayOf("**/denylistedapis/*.kt", "**/ExceptionMessageDetector*.kt", "**/util/Names.kt") kotlin { target("**/*.kt") ktfmt(ktfmtVersion).googleStyle() trimTrailingWhitespace() endWithNewline() licenseHeaderFile(rootProject.file("spotless/spotless.kt")) targetExclude("**/spotless.kt", *externalSourceGlobs) } // Externally adapted sources that should preserve their license header format("kotlinExternal", KotlinExtension::class.java) { target(*externalSourceGlobs) ktfmt(ktfmtVersion).googleStyle() trimTrailingWhitespace() endWithNewline() } kotlinGradle { ktfmt(ktfmtVersion).googleStyle() trimTrailingWhitespace() endWithNewline() licenseHeaderFile( rootProject.file("spotless/spotless.kt"), "(import|plugins|buildscript|dependencies|pluginManagement)", ) } } } val jdk = libs.versions.jdk.get().toInt() val lintJvmTargetString: String = libs.versions.lintJvmTarget.get() val runtimeJvmTargetString: String = libs.versions.runtimeJvmTarget.get() subprojects { val isChecksProject = path == ":slack-lint-checks" val jvmTargetString = if (isChecksProject) { lintJvmTargetString } else { runtimeJvmTargetString } val jvmTargetInt = jvmTargetString.toInt() pluginManager.withPlugin("java") { configure { toolchain { languageVersion.set(JavaLanguageVersion.of(jdk)) } } tasks.withType().configureEach { options.release.set(jvmTargetInt) } } pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { tasks.withType().configureEach { compilerOptions { jvmTarget.set(JvmTarget.fromTarget(jvmTargetString)) // TODO re-enable on checks if lint ever targets latest kotlin versions if (isChecksProject) { // Lint forces Kotlin 1.9 still languageVersion.set(KotlinVersion.KOTLIN_1_9) } else { allWarningsAsErrors.set(true) } } } } tasks.withType().configureEach { jvmTarget = jvmTargetString } pluginManager.withPlugin("com.vanniktech.maven.publish") { apply(plugin = "org.jetbrains.dokka") tasks.withType().configureEach { outputDirectory.set(layout.buildDirectory.dir("docs/partial")) dokkaSourceSets.configureEach { skipDeprecated.set(true) } } configure { publishToMavenCentral(automaticRelease = true) signAllPublications() } } } ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] kotlin = "2.3.21" ktfmt = "0.58" jdk = "23" # lint checks must target JDK 17, but the runtime should remain 11 lintJvmTarget = "17" runtimeJvmTarget = "11" lint = "31.12.0-alpha09" [plugins] buildConfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9" } detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" } dokka = { id = "org.jetbrains.dokka", version = "2.2.0" } lint = { id = "com.android.lint", version = "9.2.0" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version = "2.3.7" } mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.36.0" } shadow = { id = "com.gradleup.shadow", version = "9.4.1" } spotless = { id = "com.diffplug.spotless", version = "8.4.0" } [libraries] autoService-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.2.0" junit = "junit:junit:4.13.2" kotlin-metadata = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" } ktfmt = { module = "com.facebook:ktfmt", version.ref = "ktfmt" } eithernet = "com.slack.eithernet:eithernet:2.0.0" retrofit = "com.squareup.retrofit2:retrofit:3.0.0" lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" } lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "lint" } lint = { module = "com.android.tools.lint:lint", version.ref = "lint" } lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lint" } lint-testUtils = { module = "com.android.tools:testutils", version.ref = "lint" } [bundles] lintApi = ["lint-api", "lint-checks"] lintTest = ["lint", "lint-tests", "lint-testUtils"] ================================================ 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 ================================================ org.gradle.jvmargs=-Xms1g -Xmx4g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g org.gradle.parallel=true org.gradle.configureondemand=true org.gradle.caching=true org.gradle.configuration-cache=true android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,\ android.experimental.lint.missingBaselineIsEmptyBaseline,\ android.lint.useK2Uast android.experimental.lint.missingBaselineIsEmptyBaseline=true android.lint.useK2Uast=true # Always use/test K2 now systemProp.lint.use.fir.uast=true ksp.useKSP2=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true # Versioning bits GROUP=com.slack.lint POM_URL=https://github.com/slackhq/slack-lints/ POM_SCM_URL=https://github.com/slackhq/slack-lints/ POM_SCM_CONNECTION=scm:git:git://github.com/slackhq/slack-lints.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/slackhq/slack-lints.git POM_LICENCE_DIST=repo POM_DEVELOPER_ID=slackhq POM_DEVELOPER_NAME=Slack Technologies, LLC. POM_DEVELOPER_URL=https://github.com/slackhq POM_INCEPTION_YEAR=2021 VERSION_NAME=1.0.0-SNAPSHOT ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 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/HEAD/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 CLASSPATH="\\\"\\\"" # 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" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 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" \ -classpath "$CLASSPATH" \ -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 set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -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: release.sh ================================================ #!/usr/bin/env bash set -exo pipefail # Gets a property out of a .properties file # usage: getProperty $key $filename function getProperty() { grep "${1}" "$2" | cut -d'=' -f2 } NEW_VERSION=$1 SNAPSHOT_VERSION=$(getProperty 'VERSION_NAME' gradle.properties) echo "Publishing $NEW_VERSION" # Prepare release sed -i '' "s/${SNAPSHOT_VERSION}/${NEW_VERSION}/g" gradle.properties git commit -am "Prepare for release $NEW_VERSION." git tag -a "$NEW_VERSION" -m "Version $NEW_VERSION" # Publish ./gradlew publish # Prepare next snapshot echo "Restoring snapshot version $SNAPSHOT_VERSION" sed -i '' "s/${NEW_VERSION}/${SNAPSHOT_VERSION}/g" gradle.properties git commit -am "Prepare next development version." # Push it all up git push && git push --tags ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base" ], "branchPrefix": "renovate/", "gitAuthor": "OSS-Bot ", "prHourlyLimit": 10, "repositories": [ "slackhq/slack-lints" ], "packageRules": [ { "matchPackageNames": [ "renovatebot/github-action" ], "extends": [ "schedule:monthly" ] } ] } ================================================ FILE: settings.gradle.kts ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 import java.util.Locale pluginManagement { repositories { mavenCentral() google() // Last because this proxies jcenter! gradlePluginPortal() } } dependencyResolutionManagement { versionCatalogs { if (System.getenv("DEP_OVERRIDES") == "true") { val overrides = System.getenv().filterKeys { it.startsWith("DEP_OVERRIDE_") } maybeCreate("libs").apply { for ((key, value) in overrides) { val catalogKey = key.removePrefix("DEP_OVERRIDE_").lowercase(Locale.getDefault()) println("Overriding $catalogKey with $value") version(catalogKey, value) } } } } repositories { google() mavenCentral() } } rootProject.name = "slack-lints" include(":slack-lint-checks", ":slack-lint-annotations") ================================================ FILE: slack-lint-annotations/build.gradle.kts ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.mavenPublish) } ================================================ FILE: slack-lint-annotations/gradle.properties ================================================ POM_ARTIFACT_ID=slack-lint-annotations POM_NAME=Slack Lint Annotations POM_DESCRIPTION=Slack lint annotations. ================================================ FILE: slack-lint-annotations/src/main/kotlin/slack/lint/annotations/AllowUnitResult.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.annotations /** * When applied to a suspend Retrofit function, this annotation permits the function to have a * return type of [Unit], which otherwise will be flagged as an issue. */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.SOURCE) annotation class AllowUnitResult ================================================ FILE: slack-lint-annotations/src/main/kotlin/slack/lint/annotations/DoNotMock.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.annotations import java.lang.annotation.Inherited import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.CLASS /** * Annotation representing a type that should not be mocked. * * When marking a type `@DoNotMock`, you should always point to alternative testing solutions such * as standard fakes or other testing utilities. * * Mockito tests can enforce this annotation by using a custom MockMaker which intercepts creation * of mocks. */ @Inherited @Target(CLASS) @Retention(RUNTIME) annotation class DoNotMock( /** * The reason why the annotated type should not be mocked. * * This should suggest alternative APIs to use for testing objects of this type. */ val value: String = "Create a real instance instead" ) ================================================ FILE: slack-lint-annotations/src/main/kotlin/slack/lint/annotations/JavaOnly.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.annotations /** * An annotation that is used to denote methods that should not be called from any context other * than Java code. This is important for cases in APIs that support both Kotlin and Java in * different ways. * * @property reason An optional reason why this API is intended for Java only. */ annotation class JavaOnly(val reason: String = "") ================================================ FILE: slack-lint-annotations/src/main/kotlin/slack/lint/annotations/KotlinOnly.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.annotations /** * An annotation that is used to denote methods that should not be called from any context other * than Kotlin code. This is important for cases in APIs that support both Kotlin and Java in * different ways. * * @property reason An optional reason why this API is intended for Java only. */ annotation class KotlinOnly(val reason: String = "") ================================================ FILE: slack-lint-annotations/src/main/kotlin/slack/lint/annotations/MustUseNamedParams.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.annotations /** * Callers to this function must named all parameters. This is useful in cases where arguments may * change in order and you want to avoid source-breaking changes. */ @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR) @Retention(AnnotationRetention.RUNTIME) annotation class MustUseNamedParams ================================================ FILE: slack-lint-annotations/src/main/kotlin/slack/lint/annotations/RestrictCallsTo.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.annotations import java.lang.annotation.Inherited import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION /** * Annotation representing a function or property that should not be called outside of a given * [scope]. Similar to androidx's `RestrictTo` annotation but just for calls. */ @Inherited @Target(FUNCTION) @Retention(RUNTIME) annotation class RestrictCallsTo( /** The target scope. Only file is supported for now, toe-hold left for possible future scopes. */ val scope: Int = FILE ) { companion object { const val FILE = 0 } } ================================================ FILE: slack-lint-checks/build.gradle.kts ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.jvm) // Run lint on the lints! https://groups.google.com/g/lint-dev/c/q_TVEe85dgc alias(libs.plugins.lint) alias(libs.plugins.ksp) alias(libs.plugins.mavenPublish) alias(libs.plugins.shadow) alias(libs.plugins.buildConfig) } val lintKotlinVersion = KotlinVersion(2, 1, 21) buildConfig { packageName("slack.lint") useKotlinOutput { internalVisibility = true } sourceSets.getByName("test") { buildConfigField( "kotlin.KotlinVersion", "LINT_KOTLIN_VERSION", "KotlinVersion(${lintKotlinVersion.major}, ${lintKotlinVersion.minor}, ${lintKotlinVersion.patch})", ) } } lint { htmlReport = true xmlReport = true textReport = true absolutePaths = false checkTestSources = true baseline = file("lint-baseline.xml") disable += setOf("GradleDependency") fatal += setOf("LintDocExample", "LintImplPsiEquals", "UastImplementation") } val shade: Configuration = configurations.maybeCreate("compileShaded") configurations.getByName("compileOnly").extendsFrom(shade) tasks.test { // Disable noisy java applications launching during tests jvmArgs("-Djava.awt.headless=true") maxParallelForks = Runtime.getRuntime().availableProcessors() * 2 } dependencies { compileOnly(libs.bundles.lintApi) ksp(libs.autoService.ksp) implementation(libs.autoService.annotations) shade(libs.kotlin.metadata) { exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") } // Dupe the dep because the shaded version is compileOnly in the eyes of the gradle configurations testImplementation(libs.kotlin.metadata) { exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") } testImplementation(libs.bundles.lintTest) testImplementation(libs.junit) // For IDE linking of APIs testImplementation(libs.retrofit) testImplementation(libs.eithernet) { exclude(group = "org.jetbrains.kotlin") } } val kgpKotlinVersion = KotlinVersion.fromVersion(lintKotlinVersion.toString().substringBeforeLast('.')) tasks.withType().configureEach { compilerOptions { // Lint forces Kotlin (regardless of what version the project uses), so this // forces a matching language level for now. Similar to `targetCompatibility` for Java. // This should match the value in LintKotlinVersionCheckTest.kt apiVersion.set(kgpKotlinVersion) languageVersion.set(kgpKotlinVersion) } } val shadowJar = tasks.shadowJar.apply { configure { archiveClassifier.set("") configurations = listOf(shade) relocate("kotlinx.metadata", "slack.lint.shaded.kotlinx.metadata") transformers.add(ServiceFileTransformer()) } } artifacts { runtimeOnly(shadowJar) archives(shadowJar) } ================================================ FILE: slack-lint-checks/gradle.properties ================================================ POM_ARTIFACT_ID=slack-lint-checks POM_NAME=Slack Lint Checks POM_DESCRIPTION=Slack lint checks. # Opt-out flag for bundling Kotlin standard library because Lint forces its own version # See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library kotlin.stdlib.default.dependency=false ================================================ FILE: slack-lint-checks/lint-baseline.xml ================================================ ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/AlwaysNullReadOnlyVariableDetector.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtReturnExpression import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType import org.jetbrains.kotlin.psi.psiUtil.isNull import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.UMethod import org.jetbrains.uast.UVariable import org.jetbrains.uast.kotlin.isKotlin import slack.lint.util.sourceImplementation class AlwaysNullReadOnlyVariableDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UVariable::class.java, UCallExpression::class.java, UMethod::class.java) override fun createUastHandler(context: JavaContext): UElementHandler? { if (!isKotlin(context.uastFile?.lang)) return null return object : UElementHandler() { private fun isNullInitializedForReadOnlyVariable(node: UVariable): Boolean { val uastInitializer = node.uastInitializer ?: return false val isNullInitialized = uastInitializer is ULiteralExpression && uastInitializer.isNull if (!isNullInitialized) return false val sourcePsi = node.sourcePsi val isReadOnlyVariable = sourcePsi is KtProperty && !sourcePsi.isVar && !sourcePsi.hasModifier(KtTokens.OPEN_KEYWORD) return isReadOnlyVariable } override fun visitVariable(node: UVariable) { if (isNullInitializedForReadOnlyVariable(node)) { context.report( ISSUE_ALWAYS_INITIALIZE_NULL, context.getLocation(node.uastInitializer), ISSUE_ALWAYS_INITIALIZE_NULL.getBriefDescription(TextFormat.TEXT), ) } } override fun visitMethod(node: UMethod) { val sourcePsi = node.sourcePsi?.parent as? KtProperty ?: return val getter = sourcePsi.getter ?: return val isReadOnlyVariable = !sourcePsi.isVar if (isReadOnlyVariable) { // get() = null val bodyExpression = getter.bodyExpression ?: return if (bodyExpression.isNull()) { context.report( ISSUE_ALWAYS_RETURN_NULL_IN_GETTER, context.getLocation(bodyExpression), ISSUE_ALWAYS_RETURN_NULL_IN_GETTER.getBriefDescription(TextFormat.TEXT), ) } // get() { return null } val returnExpression = bodyExpression.collectDescendantsOfType() returnExpression.forEach { expression -> val returnedExpression = expression.returnedExpression ?: return@forEach if (returnedExpression.isNull()) { context.report( ISSUE_ALWAYS_RETURN_NULL_IN_GETTER, context.getLocation(returnedExpression), ISSUE_ALWAYS_RETURN_NULL_IN_GETTER.getBriefDescription(TextFormat.TEXT), ) } } } } } } companion object { val ISSUE_ALWAYS_INITIALIZE_NULL: Issue = Issue.create( "AvoidNullInitForReadOnlyVariables", "Avoid initializing read-only variable with null in Kotlin", """ Avoid unnecessary `null` initialization for read-only variables, as they can never be reassigned. \ Assigning null explicitly does not provide any real benefit and may mislead readers into thinking the value could change later. \ If the variable needs to be modified later, it's better to use `var` instead of `val`, or consider using `lateinit var` if it is guaranteed to be initialized before use. """, Category.CORRECTNESS, 6, Severity.WARNING, sourceImplementation(), ) val ISSUE_ALWAYS_RETURN_NULL_IN_GETTER: Issue = Issue.create( "AvoidReturningNullInGetter", "Avoid returning null in getter for read-only properties in Kotlin", """ Avoid defining a getter that always returns `null` for a read-only (`val`) property. \ Since `val` properties cannot be reassigned, having a getter that consistently returns `null` serves no real purpose \ and may cause confusion. If the value needs to be dynamically computed, ensure the getter returns a meaningful result. \ Otherwise, consider using a function (`fun`) instead of a property. """, Category.CORRECTNESS, 6, Severity.WARNING, sourceImplementation(), ) val ISSUES: List = listOf(ISSUE_ALWAYS_INITIALIZE_NULL, ISSUE_ALWAYS_RETURN_NULL_IN_GETTER) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/AnnotatedClassOrMethodUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.AbstractAnnotationDetector import com.android.tools.lint.detector.api.AnnotationInfo import com.android.tools.lint.detector.api.AnnotationUsageInfo import com.android.tools.lint.detector.api.AnnotationUsageType import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UElement /** * Raises a warning whenever we use deprecated methods or classes. Generally used for keeping track * of health score. */ abstract class AnnotatedClassOrMethodUsageDetector : AbstractAnnotationDetector(), SourceCodeScanner { abstract val annotationNames: List abstract val issue: Issue open val isEnabled: Boolean = true override fun applicableAnnotations(): List = if (isEnabled) annotationNames else emptyList() override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean { // If it's not enabled, no need to scan further if (!isEnabled) return false @Suppress("DEPRECATION") // METHOD_CALL_CLASS doesn't have a replacement return type == AnnotationUsageType.METHOD_CALL || type == AnnotationUsageType.METHOD_CALL_CLASS || type == AnnotationUsageType.METHOD_CALL_PARAMETER } override fun visitAnnotationUsage( context: JavaContext, element: UElement, annotationInfo: AnnotationInfo, usageInfo: AnnotationUsageInfo, ) { if (isEnabled && applicableAnnotations().contains(annotationInfo.qualifiedName)) { val issueToReport = issue val location = context.getLocation(element) report( context, issueToReport, element, location, issueToReport.getBriefDescription(TextFormat.TEXT), null, ) } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/ArgInFormattedQuantityStringResDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiLocalVariable import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import kotlin.reflect.full.safeCast import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UExpression import org.jetbrains.uast.java.JavaUCallExpression import org.jetbrains.uast.java.JavaUCompositeQualifiedExpression import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression import org.jetbrains.uast.sourcePsiElement class ArgInFormattedQuantityStringResDetector : Detector(), SourceCodeScanner { companion object { val ISSUE_ARG_IN_QUANTITY_STRING_FORMAT: Issue = Issue.create( "ArgInFormattedQuantityStringRes", "Count value in formatted string resource.", "Some languages require modifiers to counted values in written text. Consider consulting #plz-localization " + "if you are unsure if this formatted string requires a special modifier. If one is required, consider using " + "`LocalizationUtils.getFormattedCount()`. If not, suppress this warning.", Category.I18N, 6, Severity.WARNING, Implementation(ArgInFormattedQuantityStringResDetector::class.java, Scope.JAVA_FILE_SCOPE), ) val issues: List = listOf(ISSUE_ARG_IN_QUANTITY_STRING_FORMAT) } override fun getApplicableMethodNames(): List = listOf("getQuantityString") override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { // Ignore methods that aren't in a subclass of Android "Resources" if (context.evaluator.isMemberInSubClassOf(method, "android.content.res.Resources", false)) { node.valueArguments .drop(2) // Ignore the first 2 arguments passed to "getQuantityString" .forEach { arg -> when (arg) { is JavaUCompositeQualifiedExpression -> checkCall { JavaUCallExpression::class.safeCast(arg.selector) } is JavaUCallExpression -> checkCall { arg } is KotlinUQualifiedReferenceExpression -> checkCall { KotlinUFunctionCallExpression::class.safeCast(arg.selector) } is KotlinUFunctionCallExpression -> checkCall { arg } else -> checkVariable { arg } }.let { countFormatFound -> if (!countFormatFound) { context.report( ISSUE_ARG_IN_QUANTITY_STRING_FORMAT, context.getLocation(arg), "This may require a localized count modifier. If so, use `LocalizationUtils.getFormattedCount()`. Consult #plz-localization if you are unsure.", ) } } } } super.visitMethodCall(context, node, method) } /** * return true if the resolved [UCallExpression] has method name "getFormattedCount", false * otherwise */ private fun checkCall(fn: () -> UCallExpression?): Boolean { return fn()?.let { call -> "getFormattedCount" == call.methodName } ?: false } /** * return true if the resolved [UExpression] was created from the "getFormattedCount" method, * false otherwise */ private fun checkVariable(fn: () -> UExpression?): Boolean { return fn()?.let { exp -> val variable = exp.sourcePsiElement?.reference?.resolve() as? PsiLocalVariable val assignment = variable?.initializer as? PsiMethodCallExpression return assignment?.resolveMethod()?.name == "getFormattedCount" } ?: false } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/CircuitScreenDataClassDetector.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.getUMethod import org.jetbrains.kotlin.analysis.utils.isLocalClass import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtPrimaryConstructor import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import slack.lint.util.implements import slack.lint.util.sourceImplementation class CircuitScreenDataClassDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> = listOf(UClass::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitClass(node: UClass) { val sourceNode = node.sourcePsi as? KtClassOrObject if ( sourceNode != null && !sourceNode.isData() && !node.isInterface && !sourceNode.hasModifier( KtTokens.OPEN_KEYWORD ) && // Open classes cannot be "data" classes !sourceNode.hasModifier( KtTokens.INNER_KEYWORD ) && // Screens must be parcelable and inner classes cannot be parcelable !sourceNode.hasModifier( KtTokens.ABSTRACT_KEYWORD ) && // Cannot have abstract data class / object !sourceNode.hasModifier( KtTokens.SEALED_KEYWORD ) && // Cannot have sealed data class / object !sourceNode.hasModifier( KtTokens.COMPANION_KEYWORD ) && // Cannot have companion data object !node.isLocalClass() && node.implements(QUALIFIED_CIRCUIT_SCREEN) ) { val hasProperties = !node.constructors .asSequence() .mapNotNull { it.getUMethod() } .firstOrNull { it.sourcePsi is KtPrimaryConstructor } ?.uastParameters .isNullOrEmpty() val classKeyword = when (sourceNode) { is KtClass -> sourceNode.getClassKeyword() is KtObjectDeclaration -> sourceNode.getObjectKeyword() ?: return else -> return } val isObject = classKeyword?.node?.elementType == KtTokens.OBJECT_KEYWORD val originalKeyword = if (isObject) KtTokens.OBJECT_KEYWORD else KtTokens.CLASS_KEYWORD val replacement = if (hasProperties) "${KtTokens.DATA_KEYWORD} ${KtTokens.CLASS_KEYWORD}" else "${KtTokens.DATA_KEYWORD} ${KtTokens.OBJECT_KEYWORD}" val keywordLocation = context.getLocation(classKeyword) val quickFix = fix() .replace() .name("Replace with $replacement") .range(keywordLocation) .text(originalKeyword.value) .with(replacement) .reformat(true) .build() context.report(ISSUE, keywordLocation, MESSAGE, quickFix) } } } } companion object { const val QUALIFIED_CIRCUIT_SCREEN = "com.slack.circuit.runtime.screen.Screen" const val MESSAGE = "Circuit Screen implementations should be data classes or data objects, not regular classes." const val ISSUE_ID = "CircuitScreenShouldBeDataClass" const val BRIEF_DESCRIPTION = "Circuit Screen should be a data class or data object" const val EXPLANATION = """Circuit Screen implementations should be data classes or data objects to ensure proper equality, hashCode, and toString implementations. Regular classes can cause issues with screen comparison and navigation.""" val ISSUE: Issue = Issue.create( ISSUE_ID, BRIEF_DESCRIPTION, EXPLANATION, Category.CORRECTNESS, 8, Severity.ERROR, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/DaggerIssuesDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.isDuplicatedOverload import com.android.tools.lint.detector.api.isReceiver import com.intellij.lang.jvm.JvmClassKind import com.intellij.psi.PsiTypes import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UMethod import org.jetbrains.uast.getContainingUClass import slack.lint.util.sourceImplementation /** This is a simple lint check to catch common Dagger+Kotlin usage issues. */ class DaggerIssuesDetector : Detector(), SourceCodeScanner { companion object { private val ISSUE_BINDS_MUST_BE_IN_MODULE: Issue = Issue.create( "MustBeInModule", "@Binds/@Provides functions must be in modules", "@Binds/@Provides functions must be in `@Module`-annotated classes.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_BINDS_TYPE_MISMATCH: Issue = Issue.create( "BindsTypeMismatch", "@Binds parameter/return must be type-assignable", "@Binds function parameters must be type-assignable to their return types.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_RETURN_TYPE: Issue = Issue.create( "BindingReturnType", "@Binds/@Provides must have a return type", "@Binds/@Provides functions must have a return type. Cannot be void or Unit.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_RECEIVER_PARAMETER: Issue = Issue.create( "BindingReceiverParameter", "@Binds/@Provides functions cannot be extensions", "@Binds/@Provides functions cannot be extension functions. Move the receiver type to a parameter via IDE inspection (option+enter and convert to parameter).", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_BINDS_WRONG_PARAMETER_COUNT: Issue = Issue.create( "BindsWrongParameterCount", "@Binds must have one parameter", "@Binds functions require a single parameter as an input to bind.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_BINDS_MUST_BE_ABSTRACT: Issue = Issue.create( "BindsMustBeAbstract", "@Binds functions must be abstract", "@Binds functions must be abstract and cannot have function bodies.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_PROVIDES_CANNOT_BE_ABSTRACT: Issue = Issue.create( "ProvidesMustNotBeAbstract", "@Provides functions cannot be abstract", "@Provides functions cannot be abstract.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private val ISSUE_BINDS_REDUNDANT: Issue = Issue.create( "RedundantBinds", "@Binds functions should return a different type", "@Binds functions should return a different type (including annotations) than the input type.", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private const val BINDS_ANNOTATION = "dagger.Binds" private const val PROVIDES_ANNOTATION = "dagger.Provides" val ISSUES: List = listOf( ISSUE_BINDS_TYPE_MISMATCH, ISSUE_RETURN_TYPE, ISSUE_RECEIVER_PARAMETER, ISSUE_BINDS_WRONG_PARAMETER_COUNT, ISSUE_BINDS_MUST_BE_ABSTRACT, ISSUE_BINDS_REDUNDANT, ISSUE_BINDS_MUST_BE_IN_MODULE, ISSUE_PROVIDES_CANNOT_BE_ABSTRACT, ) } override fun getApplicableUastTypes() = listOf(UMethod::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitMethod(node: UMethod) { if (node.isDuplicatedOverload()) { return } if (!node.isConstructor) { val isBinds = node.hasAnnotation(BINDS_ANNOTATION) val isProvides = node.hasAnnotation(PROVIDES_ANNOTATION) if (!isBinds && !isProvides) return val containingClass = node.getContainingUClass() if (containingClass != null) { // Fine to not use MetadataJavaEvaluator since we only care about current module val moduleClass = if (context.evaluator.hasModifier(containingClass, KtTokens.COMPANION_KEYWORD)) { checkNotNull(containingClass.getContainingUClass()) { "Companion object must be nested in a class" } } else { containingClass } when { !moduleClass.hasAnnotation("dagger.Module") -> { context.report( ISSUE_BINDS_MUST_BE_IN_MODULE, context.getLocation(node), ISSUE_BINDS_MUST_BE_IN_MODULE.getBriefDescription(TextFormat.TEXT), ) return } isBinds && containingClass.isInterface -> { // Cannot have a default impl in interfaces if (node.uastBody != null) { context.report( ISSUE_BINDS_MUST_BE_ABSTRACT, context.getLocation(node.uastBody), ISSUE_BINDS_MUST_BE_ABSTRACT.getBriefDescription(TextFormat.TEXT), ) return } } containingClass.classKind == JvmClassKind.CLASS -> { val isAbstract = context.evaluator.isAbstract(node) // Binds must be abstract if (isBinds && !isAbstract) { context.report( ISSUE_BINDS_MUST_BE_ABSTRACT, context.getLocation(node), ISSUE_BINDS_MUST_BE_ABSTRACT.getBriefDescription(TextFormat.TEXT), ) return } else if (isProvides && isAbstract) { context.report( ISSUE_PROVIDES_CANNOT_BE_ABSTRACT, context.getLocation(node), ISSUE_PROVIDES_CANNOT_BE_ABSTRACT.getBriefDescription(TextFormat.TEXT), ) return } } containingClass.classKind == JvmClassKind.INTERFACE && isProvides -> { context.report( ISSUE_PROVIDES_CANNOT_BE_ABSTRACT, context.getLocation(node), ISSUE_PROVIDES_CANNOT_BE_ABSTRACT.getBriefDescription(TextFormat.TEXT), ) return } } } if (isBinds) { if (node.uastParameters.size != 1) { val locationToHighlight = if (node.uastParameters.isEmpty()) { node } else { node.parameterList } context.report( ISSUE_BINDS_WRONG_PARAMETER_COUNT, context.getLocation(locationToHighlight), ISSUE_BINDS_WRONG_PARAMETER_COUNT.getBriefDescription(TextFormat.TEXT), ) return } } val returnType = node.returnType?.takeUnless { it == PsiTypes.voidType() || context.evaluator.getTypeClass(it)?.qualifiedName == "kotlin.Unit" } if (returnType == null) { // Report missing return type val nodeLocation = node.returnTypeElement ?: node context.report( ISSUE_RETURN_TYPE, context.getLocation(nodeLocation), ISSUE_RETURN_TYPE.getBriefDescription(TextFormat.TEXT), ) return } if (node.uastParameters.isNotEmpty()) { val firstParam = node.uastParameters[0] if (firstParam.isReceiver()) { context.report( ISSUE_RECEIVER_PARAMETER, context.getNameLocation(firstParam), ISSUE_RECEIVER_PARAMETER.getBriefDescription(TextFormat.TEXT), ) return } if (isBinds) { val instanceType = firstParam.type if (instanceType == returnType) { // Check that they have different annotations, otherwise it's redundant if (firstParam.qualifiers() == node.qualifiers()) { context.report( ISSUE_BINDS_REDUNDANT, context.getLocation(node), ISSUE_BINDS_REDUNDANT.getBriefDescription(TextFormat.TEXT), ) return } } if (!returnType.isAssignableFrom(instanceType)) { context.report( ISSUE_BINDS_TYPE_MISMATCH, context.getLocation(node), ISSUE_BINDS_TYPE_MISMATCH.getBriefDescription(TextFormat.TEXT), ) } } } } } } } private fun UAnnotated.qualifiers() = uAnnotations .asSequence() .filter { it.resolve()?.hasAnnotation("javax.inject.Qualifier") == true } .toSet() } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/DeprecatedAnnotationDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.LintClient import com.android.tools.lint.detector.api.AnnotationInfo import com.android.tools.lint.detector.api.AnnotationUsageInfo import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.UastLintUtils import org.jetbrains.uast.UElement import slack.lint.util.Priorities import slack.lint.util.sourceImplementation private const val DEPRECATED_ANNOTATION_NAME_JAVA = "java.lang.Deprecated" private const val DEPRECATED_ANNOTATION_NAME_KOTLIN = "kotlin.Deprecated" private const val BRIEF_DESCRIPTION_PREFIX_DEFAULT = "This class or method" private const val BRIEF_DESCRIPTION_SUFFIX = " is deprecated; consider using an alternative." /** * Raises a warning whenever we use deprecated methods or classes. Generally used for keeping track * of health score. */ class DeprecatedAnnotationDetector : AnnotatedClassOrMethodUsageDetector() { override val annotationNames = listOf(DEPRECATED_ANNOTATION_NAME_KOTLIN, DEPRECATED_ANNOTATION_NAME_JAVA) override val issue = ISSUE_DEPRECATED_CALL // Only enable on CLI override val isEnabled: Boolean get() = !LintClient.isStudio override fun visitAnnotationUsage( context: JavaContext, element: UElement, annotationInfo: AnnotationInfo, usageInfo: AnnotationUsageInfo, ) { if (isEnabled && applicableAnnotations().contains(annotationInfo.qualifiedName)) { val issueToReport = issue val location = context.getLocation(element) val messagePrefix = usageInfo.referenced?.let(UastLintUtils.Companion::getQualifiedName) ?: BRIEF_DESCRIPTION_PREFIX_DEFAULT report( context, issueToReport, element, location, messagePrefix + BRIEF_DESCRIPTION_SUFFIX, null, ) } } companion object { private fun Implementation.toIssue(): Issue { return Issue.create( "DeprecatedCall", BRIEF_DESCRIPTION_PREFIX_DEFAULT + BRIEF_DESCRIPTION_SUFFIX, "Using deprecated classes is not advised; please consider using an alternative.", Category.CORRECTNESS, Priorities.NORMAL, Severity.WARNING, this, ) } val ISSUE_DEPRECATED_CALL = sourceImplementation().toIssue() } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/DeprecatedSqlUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import org.jetbrains.uast.UCallExpression import slack.lint.util.sourceImplementation /** * Raises a warning on direct sqlite database usage and encourages * [SqlDelight](https://cashapp.github.io/sqldelight/) usage. */ @Suppress("UnstableApiUsage") class DeprecatedSqlUsageDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext) = object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { if ( APPLICABLE_RECEIVER_TYPES.contains(node.receiverType?.canonicalText) && APPLICABLE_CALL_NAMES.contains(node.methodIdentifier?.name) ) { context.report( issue = ISSUE, location = context.getLocation(node), message = "All SQL querying should be performed using `SqlDelight`", ) } } } companion object { private fun Implementation.toIssue(): Issue { return Issue.create( id = "DeprecatedSqlUsage", briefDescription = "Use SqlDelight!", explanation = "Safer, faster, etc", category = Category.CORRECTNESS, priority = 0, severity = Severity.WARNING, implementation = this, ) } val ISSUE: Issue = sourceImplementation().toIssue() private val APPLICABLE_CALL_NAMES = listOf("query", "insert", "update", "delete", "execSQL") private val APPLICABLE_RECEIVER_TYPES = listOf("android.database.sqlite.SQLiteDatabase", "androidx.sqlite.db.SupportSQLiteDatabase") } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/DoNotCallProvidersDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UClass import org.jetbrains.uast.getParentOfType import slack.lint.util.sourceImplementation class DoNotCallProvidersDetector : Detector(), SourceCodeScanner { companion object { private val SCOPES = sourceImplementation(shouldRunOnTestSources = false) val ISSUE: Issue = Issue.create( "DoNotCallProviders", "Dagger provider methods should not be called directly by user code.", """ Dagger provider methods should not be called directly by user code. These are intended solely for use \ by Dagger-generated code and it is programmer error to call them from user code. """, Category.CORRECTNESS, 6, Severity.ERROR, SCOPES, ) private val PROVIDER_ANNOTATIONS = setOf("dagger.Binds", "dagger.Provides", "dagger.producers.Produces") private val GENERATED_ANNOTATIONS = setOf("javax.annotation.Generated", "javax.annotation.processing.Generated") } override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext) = object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val enclosingClass = node.getParentOfType() ?: return if (GENERATED_ANNOTATIONS.any(enclosingClass::hasAnnotation)) return val method = node.resolve() ?: return if (PROVIDER_ANNOTATIONS.any(method::hasAnnotation)) { context.report( ISSUE, context.getLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT), ) } } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/ExceptionMessageDetector.kt ================================================ /* * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package slack.lint import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiMethod import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.getParameterForArgument import slack.lint.util.Name import slack.lint.util.Package import slack.lint.util.isInPackageName import slack.lint.util.sourceImplementation class ExceptionMessageDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List = listOf(Check, CheckNotNull, Require, RequireNotNull).map { it.shortName } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { // We ignore other functions with the same name. if (!method.isInPackageName(KotlinPackage)) return val lazyMessage = node.valueArguments.find { node.getParameterForArgument(it)?.name == "lazyMessage" } if (lazyMessage == null) { context.report( ISSUE, node, context.getNameLocation(node), "Please specify a `lazyMessage` param for ${node.methodName}", ) } } internal companion object { val KotlinPackage = Package("kotlin") val Check = Name(KotlinPackage, "check") val CheckNotNull = Name(KotlinPackage, "checkNotNull") val Require = Name(KotlinPackage, "require") val RequireNotNull = Name(KotlinPackage, "requireNotNull") val ISSUE = Issue.create( id = "ExceptionMessage", briefDescription = "Please provide a string for the `lazyMessage` parameter", explanation = """ Calls to `check()`, `checkNotNull()`, `require()` and `requireNotNull()` \ should include a message string that can be used to debug issues \ experienced by users. When we read user-supplied logs, the line numbers are sometimes not\ sufficient to determine the cause of a bug. Inline functions can\ sometimes make it hard to determine which file threw an exception.\ Consider supplying a `lazyMessage` parameter to identify the `check()`\ or `require()` call. """, category = Category.CORRECTNESS, priority = 3, severity = Severity.ERROR, implementation = sourceImplementation(shouldRunOnTestSources = false), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/FragmentDaggerFieldInjectionDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UClass import org.jetbrains.uast.UField import slack.lint.util.implements import slack.lint.util.sourceImplementation /** * Detects that Fragments should use constructor injection in order to obtain references to its * dependencies. */ class FragmentDaggerFieldInjectionDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UField::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitField(node: UField) { if (node.isStatic || node.findAnnotation(FQN_JAVAX_INJECT) == null) return val nodeParent = node.uastParent if (nodeParent !is UClass || nodeParent.isInterface) return if (!nodeParent.isFragment()) return val issueToReport = if (nodeParent.hasConstructorInjection()) { ISSUE_FRAGMENT_CONSTRUCTOR_INJECTION_AVAILABLE } else { ISSUE_FRAGMENT_FIELD_INJECTION_USED } context.report( issueToReport, context.getLocation(node), issueToReport.getBriefDescription(TextFormat.TEXT), ) } } } private fun UClass.isFragment() = implements("androidx.fragment.app.Fragment") private fun UClass.hasConstructorInjection() = constructors.any { it.hasAnnotation(FQN_JAVAX_INJECT) || it.hasAnnotation(FQN_DAGGER_ASSISTED_INJECT) } companion object { private const val FQN_JAVAX_INJECT = "javax.inject.Inject" private const val FQN_DAGGER_ASSISTED_INJECT = "dagger.assisted.AssistedInject" private val ISSUE_FRAGMENT_CONSTRUCTOR_INJECTION_AVAILABLE: Issue = Issue.create( id = "FragmentConstructorInjection", briefDescription = "Fragment dependencies should be injected using constructor injections only.", explanation = """ This Fragment has been set up to inject its dependencies through the constructor. \ This dependency should be declared in the constructor where dagger will handle the \ injection at runtime. """, category = Category.CORRECTNESS, priority = 6, severity = Severity.ERROR, implementation = sourceImplementation(), ) private val ISSUE_FRAGMENT_FIELD_INJECTION_USED: Issue = Issue.create( id = "FragmentFieldInjection", briefDescription = "Fragment dependencies should be injected using the Fragment's constructor.", explanation = """ This dependency should be injected by dagger via the constructor. Add this field's type \ into the parameter list of the Fragment's constructor. This constructor should be annotated \ with either `@AssistedInject` or `@Inject`. Annotate with `@AssistedInject` if this \ Fragment requires runtime arguments via a `Bundle`. Annotate with `@Inject` if this \ Fragment does not require any runtime arguments. If this is an abstract class, the \ constructor does not need to be annotated with `@Inject` or `@AssistedInject`. """, category = Category.CORRECTNESS, priority = 6, severity = Severity.ERROR, implementation = sourceImplementation(), ) val issues = listOf(ISSUE_FRAGMENT_CONSTRUCTOR_INJECTION_AVAILABLE, ISSUE_FRAGMENT_FIELD_INJECTION_USED) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/GuavaPreconditionsDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.isKotlin import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UQualifiedReferenceExpression import slack.lint.util.sourceImplementation /** * Detect usages of Guava's Preconditions and recommend to use the JavaPreconditions that uses * Kotlin stdlib alternatives. */ class GuavaPreconditionsDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { if (isUsingGuavaPreconditions(node)) { if (isKotlin(node.lang)) { reportKotlin(context, node) } else { reportJavaGuavaUsage(context, node) } } } } } private fun reportJavaGuavaUsage(context: JavaContext, node: UCallExpression) { val issueToReport = ISSUE_GUAVA_CHECKS_USED val lintFix = fix() .name("Use Slack's JavaPreconditions checks") .replace() .shortenNames() .range(context.getLocation(node)) .text(createLintFixTextReplaceString(node)) .with("$FQN_SLACK_JAVA_PRECONDITIONS.${node.methodName}") .autoFix() .build() reportIssue(context, node, issueToReport, lintFix) } private fun reportKotlin(context: JavaContext, node: UCallExpression) { val issueToReport = ISSUE_GUAVA_PRECONDITIONS_USED_IN_KOTLIN val updatedKotlinCheckMethod = when (node.methodName) { METHOD_GUAVA_CHECK_STATE -> METHOD_KOTLIN_CHECK_STATE METHOD_GUAVA_CHECK_ARGUMENT -> METHOD_KOTLIN_CHECK_ARGUMENT METHOD_GUAVA_CHECK_NOT_NULL -> METHOD_KOTLIN_CHECK_NOT_NULL else -> null } val lintFix = updatedKotlinCheckMethod?.let { updatedCheckMethod -> fix() .name("Use Kotlin's standard library checks") .replace() .shortenNames() .range(context.getLocation(node)) .text(createLintFixTextReplaceString(node)) .with(updatedCheckMethod) .autoFix() .build() } reportIssue(context, node, issueToReport, lintFix) } private fun reportIssue( context: JavaContext, node: UCallExpression, issue: Issue, quickFix: LintFix? = null, ) { context.report( issue, context.getNameLocation(node), issue.getBriefDescription(TextFormat.TEXT), quickFix, ) check(true) } private fun createLintFixTextReplaceString(node: UCallExpression): String { val nodeParent = node.uastParent return if (nodeParent is UQualifiedReferenceExpression) { "${nodeParent.receiver.sourcePsi?.text}.${node.methodName}" } else { "${node.methodName}" } } private fun isUsingGuavaPreconditions(node: UCallExpression): Boolean { return node.resolve()?.containingClass?.qualifiedName == FQN_GUAVA_PRECONDITIONS } companion object { private const val FQN_GUAVA_PRECONDITIONS = "com.google.common.base.Preconditions" private const val FQN_SLACK_JAVA_PRECONDITIONS = "slack.commons.JavaPreconditions" private const val METHOD_GUAVA_CHECK_STATE = "checkState" private const val METHOD_GUAVA_CHECK_ARGUMENT = "checkArgument" private const val METHOD_GUAVA_CHECK_NOT_NULL = "checkNotNull" private const val METHOD_KOTLIN_CHECK_STATE = "check" private const val METHOD_KOTLIN_CHECK_ARGUMENT = "require" private const val METHOD_KOTLIN_CHECK_NOT_NULL = "checkNotNull" private val ISSUE_GUAVA_CHECKS_USED: Issue = Issue.create( "GuavaChecksUsed", "Use Slack's JavaPreconditions instead of Guava's Preconditions checks", """Precondition checks in Java should use Slack's internal `JavaPreconditions.kt` \ instead of Guava's Preconditions. """, Category.CORRECTNESS, 6, Severity.ERROR, implementation = sourceImplementation(true), ) private val ISSUE_GUAVA_PRECONDITIONS_USED_IN_KOTLIN: Issue = Issue.create( "GuavaPreconditionsUsedInKotlin", "Kotlin precondition checks should use the Kotlin standard library checks", """All Kotlin classes that require precondition checks should use the \ preconditions checks that are available in the Kotlin standard library in Preconditions.kt. """, Category.CORRECTNESS, 6, Severity.ERROR, implementation = sourceImplementation(true), ) val issues: List = listOf(ISSUE_GUAVA_CHECKS_USED, ISSUE_GUAVA_PRECONDITIONS_USED_IN_KOTLIN) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/InjectInJavaDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UField import org.jetbrains.uast.UMethod import org.jetbrains.uast.java.isJava import slack.lint.util.sourceImplementation /** * A simple detector that ensures that `@Inject`, `@Module`, and `@AssistedInject` are not used in * Java files in order to properly support Anvil factory generation. */ class InjectInJavaDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> { return listOf(UClass::class.java, UField::class.java, UMethod::class.java) } override fun createUastHandler(context: JavaContext): UElementHandler? { // Only applicable to Java files if (!isJava(context.uastFile?.lang)) return null return object : UElementHandler() { private fun checkNode(node: UAnnotated) { node.findInjectionAnnotation()?.let { annotation -> context.report( ISSUE, context.getNameLocation(annotation), ISSUE.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } } override fun visitClass(node: UClass) = checkNode(node) override fun visitMethod(node: UMethod) = checkNode(node) override fun visitField(node: UField) = checkNode(node) } } companion object { private const val FQCN_INJECT = "javax.inject.Inject" private const val FQCN_MODULE = "dagger.Module" private const val FQCN_ASSISTED_INJECT = "dagger.assisted.AssistedInject" private const val FQCN_ASSISTED_FACTORY = "dagger.assisted.AssistedFactory" val ISSUE: Issue = Issue.create( "InjectInJava", "Only Kotlin classes should be injected in order for Anvil to work.", """ Only Kotlin classes should be injected in order for Anvil to work. If you \ cannot easily convert this to Kotlin, consider manually providing it via a Kotlin \ `@Module`-annotated object. """, Category.CORRECTNESS, 9, Severity.ERROR, sourceImplementation(), ) private val ANNOTATIONS = setOf(FQCN_INJECT, FQCN_ASSISTED_INJECT, FQCN_MODULE, FQCN_ASSISTED_FACTORY) private fun UAnnotated.findInjectionAnnotation(): UAnnotation? { for (annotation in ANNOTATIONS) { return findAnnotation(annotation) ?: continue } return null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/JavaOnlyDetector.kt ================================================ // Copyright (C) 2020 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.UastLintUtils import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UAnonymousClass import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UCallableReferenceExpression import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.ULambdaExpression import org.jetbrains.uast.UMethod import org.jetbrains.uast.UReturnExpression import org.jetbrains.uast.getContainingUClass import org.jetbrains.uast.getContainingUMethod import org.jetbrains.uast.kotlin.isKotlin import org.jetbrains.uast.toUElementOfType import slack.lint.util.sourceImplementation /** * Logic adapted from the analogous KotlinOnlyChecker in Error-Prone. * * Consuming repos should create and use `@KotlinOnly` and `@JavaOnly` annotations from the * `slack-lint-annotations` artifact. We would normally like to consume these via properties * defining them, but lint APIs only allow reading APIs from project-local gradle.properties and not * root properties files. * * Copied recipe from https://github.com/uber/lint-checks */ class JavaOnlyDetector : Detector(), SourceCodeScanner { companion object { private const val KOTLIN_ONLY = "slack.lint.annotations.KotlinOnly" private const val JAVA_ONLY = "slack.lint.annotations.JavaOnly" private const val ISSUE_ID = "JavaOnlyDetector" private const val MESSAGE_LINT_ERROR_TITLE = "Using @JavaOnly elements in Kotlin code." private const val MESSAGE_LINT_ERROR_EXPLANATION = "This should not be called from Kotlin code" @JvmField val ISSUE = Issue.create( ISSUE_ID, MESSAGE_LINT_ERROR_TITLE, MESSAGE_LINT_ERROR_EXPLANATION, Category.INTEROPERABILITY_KOTLIN, 6, Severity.ERROR, sourceImplementation(), ) private fun anonymousTypeString(psiClass: PsiClass, type: String): String { return "Cannot create $type instances of @JavaOnly-annotated type ${UastLintUtils.getClassName(psiClass)} (in ${psiClass.containingFile.name}) " + "in Kotlin. Make a concrete class instead." } } override fun createUastHandler(context: JavaContext): UElementHandler? { // We only run this on Kotlin files, the ErrorProne analogue handles Java files. Can revisit // if we get lint in the IDE or otherwise unify if (!isKotlin(context.uastFile?.lang)) return null return object : UElementHandler() { override fun visitClass(node: UClass) { val hasJavaOnly = context.evaluator.getAnnotation(node, JAVA_ONLY) != null val hasKotlinOnly = context.evaluator.getAnnotation(node, KOTLIN_ONLY) != null if (hasJavaOnly && hasKotlinOnly) { context.report( ISSUE, context.getLocation(node.sourcePsi!!), "Cannot annotate types with both `@KotlinOnly` and `@JavaOnly`", ) return } if (hasJavaOnly || hasKotlinOnly) { return } if (node is UAnonymousClass) { if (node.uastParent.isReturnExpression() && node.isEnclosedInJavaOnlyMethod()) { return } node.baseClassType.resolve()?.let { psiClass -> context.evaluator.getAnnotation(psiClass, JAVA_ONLY)?.run { val message = anonymousTypeString(psiClass, "anonymous") context.report(ISSUE, context.getLocation(node.sourcePsi!!), message) } } return } val reportData = checkMissingSubclass(node, KOTLIN_ONLY, "KotlinOnly") ?: checkMissingSubclass(node, JAVA_ONLY, "JavaOnly") ?: return context.report( ISSUE, context.getLocation(node.sourcePsi!!), reportData.first, reportData.second, ) } private fun checkMissingSubclass( node: UClass, targetAnnotation: String, targetAnnotationSimpleName: String, ): Pair? { return listOfNotNull(node.javaPsi.superClass, *node.interfaces) .mapNotNull { psiClass -> context.evaluator.getAnnotation(psiClass, targetAnnotation)?.run { val message = "Type subclasses/implements ${UastLintUtils.getClassName(psiClass)} in ${psiClass.containingFile.name} which is annotated @$targetAnnotationSimpleName, it should also be annotated." val source = node.text return@mapNotNull message to fix() .replace() .name("Add @$targetAnnotationSimpleName") .range(context.getLocation(node.sourcePsi!!)) .shortenNames() .text(source) .with("@$targetAnnotation $source") .autoFix() .build() } } .firstOrNull() } override fun visitLambdaExpression(node: ULambdaExpression) { if (node.isReturnExpression() && node.isEnclosedInJavaOnlyMethod()) { return } node.functionalInterfaceType?.let { type -> if (type is PsiClassType) { type.resolve()?.let { psiClass -> context.evaluator.getAnnotation(psiClass, JAVA_ONLY)?.let { val message = anonymousTypeString(psiClass, "lambda") context.report(ISSUE, context.getLocation(node.sourcePsi!!), message) return } val functionalMethod = psiClass.methods.firstOrNull() ?: return functionalMethod.toUElementOfType()?.isAnnotationPresent()?.let { node.report(it, "expressed as a lambda in Kotlin") } } } } } override fun visitMethod(node: UMethod) { val hasJavaOnly = context.evaluator.getAnnotation(node, JAVA_ONLY) != null val hasKotlinOnly = context.evaluator.getAnnotation(node, KOTLIN_ONLY) != null if (hasJavaOnly && hasKotlinOnly) { context.report( ISSUE, context.getLocation(node.sourcePsi!!), "Cannot annotate functions with both `@KotlinOnly` and `@JavaOnly`", ) return } if (hasJavaOnly || hasKotlinOnly) { return } val reportData = checkMissingOverride(node, KOTLIN_ONLY, "KotlinOnly") ?: checkMissingOverride(node, JAVA_ONLY, "JavaOnly") ?: return context.report(ISSUE, context.getLocation(node), reportData.first, reportData.second) } private fun checkMissingOverride( node: UMethod, targetAnnotation: String, targetAnnotationSimpleName: String, ): Pair? { return context.evaluator.getSuperMethod(node)?.let { method -> context.evaluator.getAnnotation(method, targetAnnotation)?.run { val message = "Function overrides ${method.name} in ${ UastLintUtils.getClassName( method.containingClass!! ) } which is annotated @$targetAnnotationSimpleName, it should also be annotated." val modifier = node.modifierList.children.joinToString(separator = " ") { it.text } return@let message to fix() .replace() .name("Add @$targetAnnotationSimpleName") .range(context.getLocation(node)) .shortenNames() .text(modifier) .with("@$targetAnnotation $modifier") .autoFix() .build() } } } override fun visitCallExpression(node: UCallExpression) { node.resolve().toUElementOfType()?.isAnnotationPresent()?.let { node.report(it) } } override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) { node.resolve().toUElementOfType()?.isAnnotationPresent()?.let { node.report(it) } } private fun UExpression.report( javaOnlyMessage: String?, callString: String = "called from Kotlin", ) { val message = StringBuilder("This method should not be $callString") if (javaOnlyMessage.isNullOrBlank()) { message.append(", see its documentation for details.") } else { message.append(": $javaOnlyMessage") } context.report(ISSUE, context.getLocation(this), message.toString()) } private fun UElement?.isReturnExpression(): Boolean = this != null && uastParent is UReturnExpression private fun UElement.isEnclosedInJavaOnlyMethod(): Boolean { return getContainingUMethod()?.isAnnotationPresent() != null } private fun UMethod.isAnnotationPresent(): String? { findAnnotation(JAVA_ONLY)?.let { return it.extractValue() } getContainingUClass()?.findAnnotation(JAVA_ONLY)?.let { return it.extractValue() } context.evaluator.getPackage(this)?.let { pkg -> context.evaluator.getAnnotation(pkg, KOTLIN_ONLY)?.let { return it.extractValue() } } return null } private fun UAnnotation.extractValue(): String { return UastLintUtils.getAnnotationStringValue(this, "reason").orEmpty() } } } override fun getApplicableUastTypes(): List> { return listOf( UMethod::class.java, UCallExpression::class.java, UCallableReferenceExpression::class.java, ULambdaExpression::class.java, UClass::class.java, ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/JsonInflaterMoshiCompatibilityDetector.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.intellij.psi.PsiCapturedWildcardType import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import com.intellij.psi.PsiModifier import com.intellij.psi.PsiPrimitiveType import com.intellij.psi.PsiType import com.intellij.psi.PsiWildcardType import com.intellij.psi.util.PsiTypesUtil import org.jetbrains.kotlin.asJava.classes.KtLightClass import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.uast.UCallExpression import slack.lint.moshi.MoshiLintUtil.hasMoshiAnnotation import slack.lint.util.sourceImplementation /** * A detector that checks if a type passed into the JsonInflater.inflate/deflate methods follows * Moshi's requirements for serialization/deserialization. * * `JsonInflater` is a JSON serialization indirection we have internally at Slack. */ class JsonInflaterMoshiCompatibilityDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val method = node.resolve() ?: return if (isJsonInflaterInflateOrDeflate(node)) { when (method.name) { "inflate" -> validateInflateReturnType(context, node) "deflate" -> validateDeflateArguments(context, node) } } } } } private fun isJsonInflaterInflateOrDeflate(node: UCallExpression): Boolean { // Get the method being called val method = node.resolve() ?: return false // Check if it's from JsonInflater class val containingClass = method.containingClass?.qualifiedName ?: return false if (containingClass != FQCN_JSON_INFLATER) return false // Check if it's an inflate or deflate method return method.name == "inflate" || method.name == "deflate" } private fun validateInflateReturnType(context: JavaContext, node: UCallExpression) { // Get the return type of the inflate call val returnType = node.getExpressionType() ?: return // Skip checking primitive types and String if (isJsonPrimitive(returnType)) return // Validate if the class is Moshi-compatible validateClassForMoshiCompatibility(context, node, returnType) } private fun isJsonPrimitive(type: PsiType): Boolean { val isString = if (type is PsiClassType) { type.resolve()?.qualifiedName == FQCN_JAVA_STRING } else { false } return type is PsiPrimitiveType || isString } private fun validateDeflateArguments(context: JavaContext, node: UCallExpression) { val method = node.resolve() ?: return val parameters = method.parameterList.parameters val valueParamIndex = parameters.indexOfFirst { it.name == "value" } if (valueParamIndex == -1) return val valueArg = node.getArgumentForParameter(valueParamIndex) ?: return val valueArgType = valueArg.getExpressionType() ?: return validateClassForMoshiCompatibility(context, node, valueArgType) } private fun validateClassForMoshiCompatibility( context: JavaContext, node: UCallExpression, typeToValidate: PsiType, ) { val modelClasses = extractModelClasses(typeToValidate) if (modelClasses.any { !isMoshiCompatible(it) }) { context.report( issue = ISSUE, location = context.getLocation(node), message = ISSUE.getBriefDescription(TextFormat.TEXT), ) } } private fun extractModelClasses(type: PsiType): List { val result = mutableListOf() fun processType(t: PsiType) { val unwrapped = when (t) { is PsiWildcardType -> t.bound ?: return is PsiCapturedWildcardType -> t.wildcard.bound ?: return else -> t } val psiClass = PsiTypesUtil.getPsiClass(unwrapped) if (psiClass != null) { result.add(psiClass) } if (unwrapped is PsiClassType) { for (param in unwrapped.parameters) { processType(param) } } } processType(type) return result } private fun isMoshiCompatible(psiClass: PsiClass): Boolean { if (isPrimitiveType(psiClass)) return true if (isCollectionType(psiClass)) return true if (isAbstractOrNonPublicClass(psiClass)) return false if (psiClass.isInterface && !isSealedInterface(psiClass)) return false return psiClass.hasMoshiAnnotation() } private fun isCollectionType(psiClass: PsiClass): Boolean { val qualifiedName = psiClass.qualifiedName ?: return false return qualifiedName in listOf(FQCN_LIST, FQCN_SET, FQCN_MAP, FQCN_COLLECTION) } private fun isPrimitiveType(psiClass: PsiClass): Boolean { val qualifiedName = psiClass.qualifiedName ?: return false return qualifiedName in listOf( FQCN_JAVA_STRING, FQCN_JAVA_BOOLEAN, FQCN_JAVA_BYTE, FQCN_JAVA_CHARACTER, FQCN_JAVA_SHORT, FQCN_JAVA_INTEGER, FQCN_JAVA_LONG, FQCN_JAVA_FLOAT, FQCN_JAVA_DOUBLE, ) } private fun isAbstractOrNonPublicClass(psiClass: PsiClass): Boolean { if (psiClass.isInterface) return false // We return false for sealed classes here even though they are technically considered abstract // by PsiClass. From a Moshi perspective, sealed classes can be compatible, while abstract // classes cannot. if (isSealedClass(psiClass)) return false return (psiClass.hasModifierProperty(PsiModifier.ABSTRACT) || !psiClass.hasModifierProperty(PsiModifier.PUBLIC)) } private fun isSealedClass(psiClass: PsiClass): Boolean { if (psiClass.isInterface) return false // For Kotlin classes, check using Kotlin PSI if (psiClass is KtLightClass) { val ktClass = psiClass.kotlinOrigin return ktClass?.hasModifier(KtTokens.SEALED_KEYWORD) == true } // Fallback for Java classes (Java doesn't have sealed classes in older versions) return psiClass.hasModifierProperty(PsiModifier.SEALED) } private fun isSealedInterface(psiClass: PsiClass): Boolean { if (!psiClass.isInterface) return false // For Kotlin classes, check using Kotlin PSI if (psiClass is KtLightClass) { val ktClass = psiClass.kotlinOrigin return ktClass?.hasModifier(KtTokens.SEALED_KEYWORD) == true } // Fallback for Java classes return psiClass.hasModifierProperty(PsiModifier.SEALED) } companion object { // Fully qualified class names for relevant annotations and types private const val FQCN_JSON_INFLATER = "slack.commons.json.JsonInflater" private const val FQCN_JAVA_STRING = "java.lang.String" private const val FQCN_JAVA_BOOLEAN = "java.lang.Boolean" private const val FQCN_JAVA_BYTE = "java.lang.Byte" private const val FQCN_JAVA_CHARACTER = "java.lang.Character" private const val FQCN_JAVA_SHORT = "java.lang.Short" private const val FQCN_JAVA_INTEGER = "java.lang.Integer" private const val FQCN_JAVA_LONG = "java.lang.Long" private const val FQCN_JAVA_FLOAT = "java.lang.Float" private const val FQCN_JAVA_DOUBLE = "java.lang.Double" private const val FQCN_LIST = "java.util.List" private const val FQCN_SET = "java.util.Set" private const val FQCN_MAP = "java.util.Map" private const val FQCN_COLLECTION = "java.util.Collection" val ISSUE = Issue.create( id = "JsonInflaterMoshiIncompatibleType", "Using JsonInflater.inflate/deflate with a Moshi-incompatible type.", """ Classes used with JsonInflater.inflate/deflate must be annotated with @JsonClass or @AdaptedBy to make it \ compatible with Moshi. Additionally, it cannot be an abstract class or an interface. """, Category.CORRECTNESS, 6, Severity.ERROR, implementation = sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/MainScopeUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation 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 import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import java.util.EnumSet import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UCallableReferenceExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.kotlin.isKotlin /** * This is a [Detector] for detecting direct usages of Kotlin coroutines' * [kotlinx.coroutines.MainScope] helper function, as we want folks to use our * `slack.foundation.coroutines.android.MainScope` alternative. */ class MainScopeUsageDetector : Detector(), SourceCodeScanner { companion object { private val SCOPES = Implementation(MainScopeUsageDetector::class.java, EnumSet.of(Scope.JAVA_FILE)) val ISSUE: Issue = Issue.create( "MainScopeUsage", "Use slack.foundation.coroutines.android.MainScope.", """ Prefer using Slack's internal `MainScope` function, which supports `SlackDispatchers` and uses \ Dispatchers.Main.immediate under the hood. """, Category.CORRECTNESS, 6, Severity.ERROR, SCOPES, ) private const val COROUTINE_SCOPE_CLASS = "kotlinx.coroutines.CoroutineScopeKt" private const val MAIN_SCOPE_FUNCTION = "MainScope" } override fun getApplicableUastTypes(): List> = listOf(UCallExpression::class.java, UCallableReferenceExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler? { // Only applicable on Kotlin files if (!isKotlin(context.uastFile?.lang)) return null fun report(node: UElement) { context.report( ISSUE, context.getLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT), LintFix.create() .replace() .name("Use slack.foundation.coroutines.android.MainScope") .text("MainScope(") .with("slack.foundation.coroutines.android.MainScope(") .autoFix() .build(), ) } fun String?.isMainScope(): Boolean { return this == MAIN_SCOPE_FUNCTION } fun PsiClass?.isCoroutineScopeClass(): Boolean { if (this == null) return false return qualifiedName == COROUTINE_SCOPE_CLASS } return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { if (node.methodName.isMainScope()) { val resolved = node.resolve() ?: return if (resolved.containingClass.isCoroutineScopeClass()) { report(node) } } } override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) { if (node.callableName.isMainScope()) { val qualifierType = node.qualifierType ?: return if (qualifierType is PsiClassType && qualifierType.resolve().isCoroutineScopeClass()) report(node) } } } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/MoshiUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.getUMethod import com.android.tools.lint.detector.api.isKotlin import com.intellij.psi.PsiArrayType import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement import com.intellij.psi.PsiNamedElement import com.intellij.psi.PsiPrimitiveType import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeParameter import com.intellij.psi.util.InheritanceUtil.isInheritor import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtModifierListOwner import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtPrimaryConstructor import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtReferenceExpression import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter import org.jetbrains.kotlin.psi.psiUtil.visibilityModifier import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierTypeOrDefault import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UEnumConstant import org.jetbrains.uast.UExpression import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.UParameter import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.getContainingUFile import org.jetbrains.uast.getUastParentOfType import org.jetbrains.uast.kotlin.KotlinUAnnotation import org.jetbrains.uast.kotlin.KotlinUClassLiteralExpression import org.jetbrains.uast.toUElement import org.jetbrains.uast.toUElementOfType import slack.lint.moshi.MoshiLintUtil.hasMoshiAnnotation import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.isBoxedPrimitive import slack.lint.util.isInnerClass import slack.lint.util.isObjectOrAny import slack.lint.util.isPlatformType import slack.lint.util.isString import slack.lint.util.removeNode import slack.lint.util.snakeToCamel import slack.lint.util.sourceImplementation import slack.lint.util.toScreamingSnakeCase import slack.lint.util.unwrapSimpleNameReferenceExpression /** A detector for a number of issues related to Moshi usage. */ // TODO could we detect call expressions to JsonInflater/Gson/Moshi and check if // the type going in is a moshi class and meets these requirements? class MoshiUsageDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UClass::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { val slackEvaluator = MetadataJavaEvaluator(context.file.name, context.evaluator) return object : UElementHandler() { override fun visitClass(node: UClass) { // Enums get checked in both languages because it's easy enough if (node.isEnum) { checkEnum(context, node) return } if (node.isAnnotationType) { checkJsonQualifierAnnotation(context, node) } context.uastFile?.lang?.let { language -> if (!isKotlin(language)) return } val adaptedByAnnotation = node.findAnnotation(FQCN_ADAPTED_BY) val jsonClassAnnotation = node.findAnnotation(FQCN_JSON_CLASS) if (adaptedByAnnotation != null) { if (jsonClassAnnotation != null) { // Report both context.report( ISSUE_DOUBLE_CLASS_ANNOTATION, context.getNameLocation(adaptedByAnnotation), ISSUE_DOUBLE_CLASS_ANNOTATION.getBriefDescription(TextFormat.TEXT), fix().removeNode(context, adaptedByAnnotation.sourcePsi!!), ) context.report( ISSUE_DOUBLE_CLASS_ANNOTATION, context.getNameLocation(jsonClassAnnotation), ISSUE_DOUBLE_CLASS_ANNOTATION.getBriefDescription(TextFormat.TEXT), fix().removeNode(context, jsonClassAnnotation.sourcePsi!!), ) } validateAdaptedByAnnotation(context, slackEvaluator, adaptedByAnnotation) return } checkSealedClass(node) if (jsonClassAnnotation == null) return val ktModifierListOwner = node.sourcePsi as KtModifierListOwner val visibility = ktModifierListOwner.visibilityModifierTypeOrDefault() if (visibility !in REQUIRED_VISIBILITIES) { val visibilityElement = ktModifierListOwner.visibilityModifier() val location = context.getLocation(visibilityElement!!) context.report( ISSUE_VISIBILITY, location, ISSUE_VISIBILITY.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Make 'internal'") .range(location) .shortenNames() .text(visibility.value) .with("internal") .autoFix() .build(), ) } // Check that generateAdapter is false var usesCustomGenerator = false // Check the `JsonClass.generator` property first // If generator is an empty string (the default), then it's a class. // If it were something else, a generator is claiming to generate something for it // and we choose to ignore it in that case. // This can sometimes come back as the default empty String and sometimes as null if not // defined val generatorExpression = jsonClassAnnotation.findDeclaredAttributeValue("generator") val generator = generatorExpression?.evaluate() as? String if (generator != null) { if (generator.isBlank()) { context.report( ISSUE_BLANK_GENERATOR, context.getLocation(generatorExpression), ISSUE_BLANK_GENERATOR.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } else { usesCustomGenerator = true if (generator.startsWith("sealed:")) { val typeLabel = generator.removePrefix("sealed:") if (typeLabel.isBlank()) { context.report( ISSUE_BLANK_TYPE_LABEL, context.getLocation(generatorExpression), ISSUE_BLANK_TYPE_LABEL.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } if (!slackEvaluator.isSealed(node)) { context.report( ISSUE_SEALED_MUST_BE_SEALED, context.getNameLocation(node), ISSUE_SEALED_MUST_BE_SEALED.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } } } } val generateAdapterExpression = jsonClassAnnotation.findAttributeValue("generateAdapter") val generateAdapter = generateAdapterExpression?.evaluate() as? Boolean? ?: false val isData = slackEvaluator.isData(node) if (!generateAdapter) { // If it's a data class always report these because it's probably user error if (isData) { context.report( ISSUE_GENERATE_ADAPTER_SHOULD_BE_TRUE, context.getLocation(generateAdapterExpression as UExpression), ISSUE_GENERATE_ADAPTER_SHOULD_BE_TRUE.getBriefDescription(TextFormat.TEXT), fix() .replace() .name("Set to true") .text(generateAdapterExpression.asSourceString()) .with("true") .autoFix() .build(), ) } // It's ok if we're just doing generateAdapter = false as these might be for R8 reasons return } // If they're using a custom generator, peace of out this if (usesCustomGenerator) return val isUnsupportedType = slackEvaluator.isObject(node) || node.isAnnotationType || node.isInterface || node.isInnerClass(slackEvaluator) || slackEvaluator.isAbstract(node) if (isUnsupportedType) { if (slackEvaluator.isObject(node)) { // Kotlin objects are ok in certain cases, so we give a more specific error message context.report( ISSUE_OBJECT, context.getNameLocation(jsonClassAnnotation), ISSUE_OBJECT.getBriefDescription(TextFormat.TEXT), fix().removeNode(context, jsonClassAnnotation.sourcePsi!!), ) } else { context.report( ISSUE_UNSUPPORTED_TYPE, context.getNameLocation(jsonClassAnnotation), ISSUE_UNSUPPORTED_TYPE.getBriefDescription(TextFormat.TEXT), fix().removeNode(context, jsonClassAnnotation.sourcePsi!!), ) } return } if (!isData) { // These should be data classes unless there's a very specific reason not to context.report( ISSUE_USE_DATA, context.getNameLocation(node), ISSUE_USE_DATA.getBriefDescription(TextFormat.TEXT), ) } // Visit primary constructor properties val primaryConstructor = node.constructors .asSequence() .mapNotNull { it.getUMethod() } .firstOrNull { it.sourcePsi is KtPrimaryConstructor } if (primaryConstructor == null) { context.report( ISSUE_MISSING_PRIMARY, context.getNameLocation(node), ISSUE_MISSING_PRIMARY.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) return } val pConstructorPsi = primaryConstructor.sourcePsi as KtPrimaryConstructor val constructorVisibility = pConstructorPsi.visibilityModifierTypeOrDefault() if (constructorVisibility !in REQUIRED_VISIBILITIES) { val visibilityElement = pConstructorPsi.visibilityModifier()!! val location = context.getLocation(visibilityElement) context.report( ISSUE_PRIVATE_CONSTRUCTOR, location, ISSUE_PRIVATE_CONSTRUCTOR.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Make constructor 'internal'") .range(location) .shortenNames() .text(constructorVisibility.value) .with("internal") .autoFix() .build(), ) } val jsonNames = mutableMapOf() for (parameter in primaryConstructor.uastParameters) { val sourcePsi = parameter.sourcePsi val defaultValueExpression = if (sourcePsi is KtParameter) { sourcePsi.defaultValue } else { null } val hasDefaultValue = defaultValueExpression != null if ( parameter.uAnnotations.any { it.qualifiedName == "kotlin.jvm.Transient" } && !hasDefaultValue ) { ISSUE_TRANSIENT_NEEDS_INIT context.report( ISSUE_TRANSIENT_NEEDS_INIT, context.getLocation(parameter as UElement), ISSUE_TRANSIENT_NEEDS_INIT.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } if (sourcePsi is KtParameter && sourcePsi.isPropertyParameter()) { if (sourcePsi.isMutable) { val location = context.getLocation(sourcePsi.valOrVarKeyword!!) context.report( ISSUE_VAR_PROPERTY, location, ISSUE_VAR_PROPERTY.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Make ${parameter.name} 'val'") .range(location) .shortenNames() .text("var") .with("val") .autoFix() .build(), ) } val paramVisibility = sourcePsi.visibilityModifierTypeOrDefault() if (paramVisibility !in REQUIRED_VISIBILITIES) { val visibilityElement = sourcePsi.visibilityModifier()!! val location = context.getLocation(visibilityElement) context.report( ISSUE_PRIVATE_PARAMETER, location, ISSUE_PRIVATE_PARAMETER.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Make ${parameter.name} 'internal'") .range(location) .shortenNames() .text(paramVisibility.value) .with("internal") .autoFix() .build(), ) } val propertyAdaptedByAnnotation = parameter.findAnnotation(FQCN_ADAPTED_BY) if (propertyAdaptedByAnnotation != null) { validateAdaptedByAnnotation(context, slackEvaluator, propertyAdaptedByAnnotation) } val shouldCheckPropertyType = propertyAdaptedByAnnotation == null && // If any JsonQualifier annotations are present, defer to whatever adapter looks at // them parameter.uAnnotations.none { it.resolve()?.hasAnnotation(FQCN_JSON_QUALIFIER) == true } if (shouldCheckPropertyType) { checkMoshiType( context, slackEvaluator, parameter.type, parameter, parameter.typeReference!!, defaultValueExpression, nestedGenericCheck = false, ) } // Note: this is a sort of round-about way to get all annotations because just calling // UAnnotatedElement.uAnnotations will only return one annotation with a matching name // when we want to check for possible duplicates here. val jsonAnnotations = (parameter.sourcePsi as KtParameter).annotationEntries.mapNotNull { annotationEntry -> (annotationEntry.toUElement() as KotlinUAnnotation).takeIf { it.qualifiedName == FQCN_JSON } } var jsonAnnotationToValidate: UAnnotation? = null if (jsonAnnotations.isNotEmpty()) { if (jsonAnnotations.size != 1) { // If we have multiple @Json annotations, likely one is correct (i.e. @Json) and the // the others are someone guessing to use a site target but not realizing it's not // necessary. So, we suggest removing any with site targets. // If, for some reason, _all_ of them are using site targets, then keep one and let // a later detector suggest removing the site target. val annotationToKeep = jsonAnnotations.find { it.sourcePsi.useSiteTarget == null } ?: jsonAnnotations[0] for (annotation in jsonAnnotations) { if (annotation == annotationToKeep) continue // Suggest removing the entire extra annotation context.report( ISSUE_JSON_SITE_TARGET, context.getLocation(annotation), ISSUE_JSON_SITE_TARGET.getBriefDescription(TextFormat.TEXT), quickfixData = fix().removeNode(context, annotation.sourcePsi), ) } } else { // Check for site targets, which are redundant val jsonAnnotation = jsonAnnotations[0] jsonAnnotation.sourcePsi.useSiteTarget?.let { siteTarget -> context.report( ISSUE_JSON_SITE_TARGET, context.getLocation(siteTarget), ISSUE_JSON_SITE_TARGET.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .removeNode(context, jsonAnnotation.sourcePsi, text = "${siteTarget.text}:"), ) } jsonAnnotationToValidate = jsonAnnotation } } validateJsonName(context, parameter, jsonAnnotationToValidate, jsonNames) if (jsonAnnotationToValidate == null) { // If they already have an `@Json` annotation, don't warn because the IDE will already // nag them separately about this with better renaming tools. val name = sourcePsi.name ?: continue val camelCase = name.snakeToCamel() if (name != camelCase) { val propKeyword = sourcePsi.valOrVarKeyword!!.text context.report( ISSUE_SNAKE_CASE, context.getNameLocation(parameter as UElement), ISSUE_SNAKE_CASE.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Add @Json(name = \"$name\") and rename to '$camelCase'") .range(context.getLocation(sourcePsi)) .shortenNames() .text("$propKeyword $name") .with("@$FQCN_JSON(name = \"$name\") $propKeyword $camelCase") .autoFix() .build(), ) } } } else { if (!hasDefaultValue) { context.report( ISSUE_PARAM_NEEDS_INIT, context.getLocation(parameter as UElement), ISSUE_PARAM_NEEDS_INIT.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } } } } private fun checkSealedClass(node: UClass) { val typeLabelAnnotation = node.getAnnotation(FQCN_TYPE_LABEL) val defaultObjectAnnotation = node.getAnnotation(FQCN_DEFAULT_OBJECT) if (typeLabelAnnotation != null && defaultObjectAnnotation != null) { // Report both context.report( ISSUE_DOUBLE_TYPE_LABEL, context.getNameLocation(typeLabelAnnotation), ISSUE_DOUBLE_TYPE_LABEL.getBriefDescription(TextFormat.TEXT), fix().removeNode(context, typeLabelAnnotation), ) context.report( ISSUE_DOUBLE_TYPE_LABEL, context.getNameLocation(defaultObjectAnnotation), ISSUE_DOUBLE_TYPE_LABEL.getBriefDescription(TextFormat.TEXT), fix().removeNode(context, defaultObjectAnnotation), ) return } val isTypeLabeled = typeLabelAnnotation != null if (isTypeLabeled && node.hasTypeParameters()) { context.report( ISSUE_GENERIC_SEALED_SUBTYPE, context.getLocation((node.sourcePsi as KtClass).typeParameterList!!), ISSUE_GENERIC_SEALED_SUBTYPE.getBriefDescription(TextFormat.TEXT), ) } val isDefaultObjectLabeled = defaultObjectAnnotation != null val isAnnotatedWithTypeLabelOrDefaultObject = isTypeLabeled || isDefaultObjectLabeled // Collect all superTypes since interfaces can be sealed too! // Filter out types in other packages as the compiler will enforce that for us. val currentPackage = node.getContainingUFile()?.packageName ?: return val sealedSuperTypeFound = node.superTypes .asSequence() .mapNotNull { slackEvaluator.getTypeClass(it)?.toUElementOfType() } .filter { it.getContainingUFile()?.packageName == currentPackage } .firstOrNull { superType -> if (slackEvaluator.isSealed(superType)) { val superJsonClassAnnotation = superType.findAnnotation(FQCN_JSON_CLASS) if (superJsonClassAnnotation != null) { val generatorExpression = superJsonClassAnnotation.findAttributeValue("generator") val generator = generatorExpression?.evaluate() as? String if (generator != null) { if (generator.startsWith("sealed:")) { if (!isAnnotatedWithTypeLabelOrDefaultObject) { context.report( ISSUE_MISSING_TYPE_LABEL, context.getNameLocation(node), ISSUE_MISSING_TYPE_LABEL.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) return@firstOrNull true } else { return@firstOrNull true } } } } } false } if (sealedSuperTypeFound != null) return // If we've reached here and have annotations, something is wrong if (isAnnotatedWithTypeLabelOrDefaultObject) { if (typeLabelAnnotation != null) { context.report( ISSUE_INAPPROPRIATE_TYPE_LABEL, context.getNameLocation(typeLabelAnnotation), ISSUE_INAPPROPRIATE_TYPE_LABEL.getBriefDescription(TextFormat.TEXT), quickfixData = fix().removeNode(context, typeLabelAnnotation), ) } if (defaultObjectAnnotation != null) { context.report( ISSUE_INAPPROPRIATE_TYPE_LABEL, context.getNameLocation(defaultObjectAnnotation), ISSUE_INAPPROPRIATE_TYPE_LABEL.getBriefDescription(TextFormat.TEXT), quickfixData = fix().removeNode(context, defaultObjectAnnotation), ) } } } } } private fun validateAdaptedByAnnotation( context: JavaContext, evaluator: MetadataJavaEvaluator, adaptedByAnnotation: UAnnotation, ) { // Check the adapter is a valid adapter type val adapterAttribute = (adaptedByAnnotation.findAttributeValue("adapter") as? KotlinUClassLiteralExpression) ?: return val targetType = adapterAttribute.type ?: return val targetClass = evaluator.getTypeClass(targetType) ?: return val implementsAdapter = isInheritor(targetClass, true, FQCN_JSON_ADAPTER) || isInheritor(targetClass, true, FQCN_JSON_ADAPTER_FACTORY) if (!implementsAdapter) { context.report( ISSUE_ADAPTED_BY_REQUIRES_ADAPTER, context.getLocation(adapterAttribute), ISSUE_ADAPTED_BY_REQUIRES_ADAPTER.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } else if (!targetClass.hasAnnotation("androidx.annotation.Keep")) { context.report( ISSUE_ADAPTED_BY_REQUIRES_KEEP, context.getLocation(adapterAttribute), ISSUE_ADAPTED_BY_REQUIRES_KEEP.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } } private fun checkMoshiType( context: JavaContext, evaluator: MetadataJavaEvaluator, psiType: PsiType, parameter: UParameter, typeNode: UElement, defaultValueExpression: KtExpression?, nestedGenericCheck: Boolean = true, ) { if (psiType is PsiPrimitiveType) return if (psiType is PsiArrayType) { val componentType = psiType.componentType val componentTypeName = if (componentType is PsiPrimitiveType) { componentType.boxedTypeName!!.let { if (it == "java.lang.Integer") "Int" else it.removePrefix("java.lang.") } } else { typeNode.sourcePsi!!.text.trim().removePrefix("Array<").removeSuffix(">") } val replacement = "List<$componentTypeName>" context.report( ISSUE_ARRAY, context.getLocation(typeNode), ISSUE_ARRAY.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Change to $replacement") .range(context.getLocation(typeNode)) .shortenNames() .text(typeNode.sourcePsi!!.text) .with(replacement) .autoFix() .build() .takeUnless { nestedGenericCheck }, ) return } if (psiType !is PsiClassType) return val psiClass = evaluator.getTypeClass(psiType) ?: error( "Could not load class for ${psiType.className} on ${parameter.getUastParentOfType()!!.name}.${parameter.name}" ) if (psiClass is PsiTypeParameter) return if (psiClass.isString() || psiClass.isObjectOrAny() || psiClass.isBoxedPrimitive()) return if (psiClass.isEnum) { if (!psiClass.hasMoshiAnnotation()) { context.report( ISSUE_ENUM_PROPERTY_COULD_BE_MOSHI, context.getLocation(typeNode), ISSUE_ENUM_PROPERTY_COULD_BE_MOSHI.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } else if ( defaultValueExpression is KtReferenceExpression || defaultValueExpression is KtQualifiedExpression ) { val defaultValueText = defaultValueExpression.text if ("UNKNOWN" in defaultValueText) { val oldText = " = $defaultValueText" context.report( ISSUE_ENUM_PROPERTY_DEFAULT_UNKNOWN, context.getLocation(defaultValueExpression), ISSUE_ENUM_PROPERTY_DEFAULT_UNKNOWN.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Remove '$oldText'") .range(context.getLocation(parameter as UElement)) .shortenNames() .text(oldText) .with("") .autoFix() .build() .takeUnless { nestedGenericCheck }, ) } } return } // Check collections val isJsonCollection = psiClass.qualifiedName == "java.util.List" || psiClass.qualifiedName == "java.util.Collection" || psiClass.qualifiedName == "java.util.Set" || psiClass.qualifiedName == "java.util.Map" if (isJsonCollection) { // Do a fuzzy check for mutability. PSI doesn't tell us if they're Kotlin mutable types so we // look at the source manually. val source = typeNode.sourcePsi!!.text val correctedImmutableType = when { source.startsWith("MutableCollection") -> "Collection" source.startsWith("MutableList") -> "List" source.startsWith("MutableSet") -> "Set" source.startsWith("MutableMap") -> "Map" else -> null } if (correctedImmutableType != null) { context.report( ISSUE_MUTABLE_COLLECTIONS, context.getLocation(typeNode), ISSUE_MUTABLE_COLLECTIONS.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name("Change to $correctedImmutableType") .range(context.getLocation(typeNode)) .shortenNames() .text("Mutable$correctedImmutableType<") .with("$correctedImmutableType<") .autoFix() .build() .takeUnless { nestedGenericCheck }, ) } for (typeArg in psiType.parameters) { // It's generic, check each generic. // TODO we currently report the whole type even if it's just one bad generic. Ideally // we just underline the generic type but as mentioned higher up this is hard in PSI. // Requires separately mapping PSI type args to the modeled versions. Example below: // val argsPsi = (typeNode.sourcePsi!!.children[0] as KtUserType).typeArguments checkMoshiType(context, evaluator, typeArg, parameter, typeNode, null) } return } when { isInheritor(psiClass, "java.util.Collection") -> { // A second, more flexible collection check that can suggest better alternatives val source = typeNode.sourcePsi!!.text val type = when { "Collection" in source -> "Collection" "List" in source -> "List" "Set" in source -> "Set" else -> null } val fix = if (type == null) { null } else { fix() .replace() .name("Change to $type") .range(context.getLocation(typeNode)) .shortenNames() .text(source.substringBefore("<")) .with(type) .autoFix() .build() .takeUnless { nestedGenericCheck } } context.report( ISSUE_NON_MOSHI_CLASS_COLLECTION, context.getLocation(typeNode), ISSUE_NON_MOSHI_CLASS_COLLECTION.getBriefDescription(TextFormat.TEXT) .withHint(psiClass.name), quickfixData = fix, ) } isInheritor(psiClass, "java.util.Map") -> { // A second, more flexible map check that can suggest better alternatives context.report( ISSUE_NON_MOSHI_CLASS_MAP, context.getLocation(typeNode), ISSUE_NON_MOSHI_CLASS_MAP.getBriefDescription(TextFormat.TEXT).withHint(psiClass.name), quickfixData = fix() .replace() .name("Change to Map") .range(context.getLocation(typeNode)) .shortenNames() .text(typeNode.sourcePsi!!.text.substringBefore("<")) .with("Map") .autoFix() .build() .takeUnless { nestedGenericCheck }, ) } psiClass.isPlatformType() -> { // Warn because this isn't supported // Eventually error when gson interop is out context.report( ISSUE_NON_MOSHI_CLASS_PLATFORM, context.getLocation(typeNode), ISSUE_NON_MOSHI_CLASS_PLATFORM.getBriefDescription(TextFormat.TEXT) .withHint(psiClass.name), quickfixData = null, ) } psiClass.hasMoshiAnnotation() -> { if (psiType.hasParameters()) { for (typeArg in psiType.parameters) { // It's generic, check each generic. // TODO we currently report the whole type even if it's just one bad generic. Ideally // we just underline the generic type but as mentioned higher up this is hard in PSI checkMoshiType(context, evaluator, typeArg, parameter, typeNode, null) } } else { return } } else -> { if (psiClass.qualifiedName?.startsWith("slack") == true) { // Slack class, suggest making it JsonClass context.report( ISSUE_NON_MOSHI_CLASS_INTERNAL, context.getLocation(typeNode), ISSUE_NON_MOSHI_CLASS_INTERNAL.getBriefDescription(TextFormat.TEXT) .withHint(psiClass.name), quickfixData = null, ) } else { // Other class, error not supported context.report( ISSUE_NON_MOSHI_CLASS_EXTERNAL, context.getLocation(typeNode), ISSUE_NON_MOSHI_CLASS_EXTERNAL.getBriefDescription(TextFormat.TEXT) .withHint(psiClass.name), quickfixData = null, ) } } } } /** * Validates @Json name annotation usage and also checks for `@SerializedName` usage. Returns the * Json.name value, if any. */ private fun validateJsonName( context: JavaContext, member: T, jsonAnnotation: UAnnotation?, seenNames: MutableMap, ) where T : UAnnotated, T : PsiNamedElement { var jsonNameValue: String? = null if (jsonAnnotation != null) { val jsonNameAttr = jsonAnnotation.findAttributeValue("name") val jsonName = jsonNameAttr?.evaluate() as? String when { jsonName == null -> { // Ignored, sometimes UAST stubs an incomplete annotation without members } jsonName.isBlank() -> { context.report( ISSUE_BLANK_JSON_NAME, context.getLocation(jsonNameAttr), ISSUE_BLANK_JSON_NAME.getBriefDescription(TextFormat.TEXT), ) } jsonName == member.name -> { context.report( ISSUE_REDUNDANT_JSON_NAME, context.getLocation(jsonNameAttr), ISSUE_REDUNDANT_JSON_NAME.getBriefDescription(TextFormat.TEXT), quickfixData = fix().removeNode(context, jsonAnnotation.sourcePsi!!), ) } else -> { // Save this to compare to SerializedName later jsonNameValue = jsonName } } } val jsonName = jsonNameValue ?: member.name!! seenNames.put(jsonName, member)?.let { existingMember -> // Report both context.report( ISSUE_DUPLICATE_JSON_NAME, context.getNameLocation(member as PsiElement), "Name '$jsonName' is duplicated by member '${existingMember.name}'.", ) context.report( ISSUE_DUPLICATE_JSON_NAME, context.getNameLocation(existingMember), "Name '$jsonName' is duplicated by member '${member.name}'.", ) } // Check for a leftover `@SerializedName` member.findAnnotation(FQCN_SERIALIZED_NAME)?.let { serializedName -> val name = serializedName.findAttributeValue("value")?.evaluate() as String val alternateCount = (serializedName.findAttributeValue("alternate")?.sourcePsi as? KtCollectionLiteralExpression) ?.getInnerExpressions() ?.size ?: -1 val hasAlternates = alternateCount > 0 var fix: LintFix? = null // If alternates are present, offer no suggestion because there's no equivalent in @Json // If both are present and have the same value, offer to delete it // If both are present with different values, offer no fix, developer needs to reconcile // If only @SerializedName is present, offer to replace with @Json if (jsonAnnotation != null) { if (jsonNameValue == name && !hasAlternates) { fix = fix().removeNode(context, serializedName.sourcePsi!!) } } else if (!hasAlternates) { fix = fix() .replace() .name("Replace with @Json(name = \"$name\")") .range(context.getLocation(serializedName)) .shortenNames() .text(serializedName.sourcePsi!!.text) .with("@$FQCN_JSON(name = \"$name\")") .autoFix() .build() } context.report( ISSUE_SERIALIZED_NAME, context.getLocation(serializedName), ISSUE_SERIALIZED_NAME.getBriefDescription(TextFormat.TEXT), quickfixData = fix, ) } } /** A simple check for `@JsonQualifier` annotation classes. */ private fun checkJsonQualifierAnnotation(context: JavaContext, node: UClass) { if (!node.hasAnnotation(FQCN_JSON_QUALIFIER)) return // JsonQualifier annotations must have RUNTIME retention and support targeting FIELD // Try both the Kotlin and Java annotations. In Kotlin we try both val retentionAnnotationPair: Pair val targetAnnotationPair: Pair val isKotlin = isKotlin(node.language) if (isKotlin) { retentionAnnotationPair = "kotlin.annotation.Retention" to "value" targetAnnotationPair = "kotlin.annotation.Target" to "allowedTargets" } else { retentionAnnotationPair = "java.lang.annotation.Retention" to "value" targetAnnotationPair = "java.lang.annotation.Target" to "value" } node.findAnnotation(retentionAnnotationPair.first)?.let { retentionAnnotation -> val retentionValue = retentionAnnotation .findAttributeValue(retentionAnnotationPair.second) ?.unwrapSimpleNameReferenceExpression() .run { this ?: if (isKotlin) { // Undefined would be weird but ok, default again is RUNTIME return@let } else { // Always required in Java error("Not possible") } } if (retentionValue.identifier != "RUNTIME") { val fix = if (isKotlin) { // Fix is just to remove it because RUNTIME is the default in Kotlin. fix().removeNode(context, retentionAnnotation.sourcePsi!!) } else { // Java we need to change it fix() .replace() .name("Replace with RUNTIME") .range(context.getLocation(retentionValue)) .shortenNames() .text(retentionValue.identifier) .with("RUNTIME") .autoFix() .build() } context.report( ISSUE_QUALIFIER_RETENTION, context.getLocation(retentionAnnotation.sourcePsi!!), ISSUE_QUALIFIER_RETENTION.getBriefDescription(TextFormat.TEXT), quickfixData = fix, ) } } ?: run { if (!isKotlin) { // Default in Kotlin when unannotated is RUNTIME but not in Java! // TODO can we add it for them? context.report( ISSUE_QUALIFIER_RETENTION, context.getNameLocation(node), ISSUE_QUALIFIER_RETENTION.getBriefDescription(TextFormat.TEXT), ) } } // It's ok if Target is missing, default in both languages includes FIELD node.findAnnotation(targetAnnotationPair.first)?.let { targetAnnotation -> val targetValues = when ( val targetsAttr = targetAnnotation.findAttributeValue(targetAnnotationPair.second)!! ) { is UCallExpression -> { // Covers all of these cases // @Target(FIELD, PROPERTY) // @Target([FIELD, PROPERTY]) // @Target({FIELD, PROPERTY}) targetsAttr.valueArguments } is UReferenceExpression -> { // @Target(FIELD) listOf(targetsAttr) } else -> { error("Unrecognized annotation attr value: $targetsAttr") } } if (targetValues.none { it.unwrapSimpleNameReferenceExpression().identifier == "FIELD" }) { context.report( ISSUE_QUALIFIER_TARGET, context.getLocation(targetAnnotation.sourcePsi!!), ISSUE_QUALIFIER_TARGET.getBriefDescription(TextFormat.TEXT), ) } } } /** * A simple check for a number of issues related to enum use in Moshi. * * In short - enums serialized with Moshi _must_ meet the following requirements: * - Be annotated with `@JsonClass`. * - Reserve their first member as `UNKNOWN` for handling unrecognized enums. * * This lint will attempt to detect if an enum is used with Moshi and check these requirements. * They should not generate adapters (i.e. `JsonClass.generateAdapter` should be `false`) but are * exempt from that requirement and the requirements of this lint in general if they define a * custom `JsonClass.generator` value. */ private fun checkEnum(context: JavaContext, node: UClass) { val jsonClassAnnotation = node.findAnnotation(FQCN_JSON_CLASS) val constants = node.uastDeclarations.filterIsInstance() val hasJsonAnnotatedConstant = constants.any { it.hasAnnotation(FQCN_JSON) } val isPresumedMoshi = jsonClassAnnotation != null || hasJsonAnnotatedConstant // If it's not annotated and no members have @Json, it's not a moshi class if (!isPresumedMoshi) return // Check that generateAdapter is false var usesCustomGenerator = false if (jsonClassAnnotation != null) { // Check the `JsonClass.generator` property first // If generator is an empty string (the default), then it's a standard enum. // If it were something else, a generator is claiming to generate something for it // and we choose to ignore it in that case. // This can sometimes come back as the default empty String and sometimes as null if not // defined val generator = jsonClassAnnotation.findAttributeValue("generator")?.evaluate() as? String usesCustomGenerator = !generator.isNullOrBlank() if (!usesCustomGenerator) { val generateAdapter = jsonClassAnnotation.findAttributeValue("generateAdapter") as ULiteralExpression if (generateAdapter.evaluate() != false) { context.report( ISSUE_ENUM_JSON_CLASS_GENERATED, context.getLocation(generateAdapter), ISSUE_ENUM_JSON_CLASS_GENERATED.getBriefDescription(TextFormat.TEXT), fix() .replace() .name("Set to false") .text(generateAdapter.asSourceString()) .with("false") .autoFix() .build(), ) } } } else { // If an @Json is present but not @JsonClass, suggest it context.report( ISSUE_ENUM_JSON_CLASS_MISSING, context.getNameLocation(node), ISSUE_ENUM_JSON_CLASS_MISSING.getBriefDescription(TextFormat.TEXT), // TODO can we add it for them? quickfixData = null, ) } // If they're using a custom generator, peace of out this if (usesCustomGenerator) return // Visit members, ensure first is UNKNOWN if annotated val unknownIndex = constants.indexOfFirst { it.name == "UNKNOWN" } if (unknownIndex == -1) { context.report( ISSUE_ENUM_UNKNOWN, context.getNameLocation(node), ISSUE_ENUM_UNKNOWN.getBriefDescription(TextFormat.TEXT), quickfixData = null, // TODO can we add an enum for them? ) } else { val constant = constants[unknownIndex] if (unknownIndex != 0) { context.report( ISSUE_ENUM_UNKNOWN, context.getNameLocation(constant), ISSUE_ENUM_UNKNOWN.getBriefDescription(TextFormat.TEXT), quickfixData = null, // TODO can we reorder it for them? ) } else { val jsonAnnotation = constant.getAnnotation(FQCN_JSON) if (jsonAnnotation != null) { val source = jsonAnnotation.text context.report( ISSUE_ENUM_ANNOTATED_UNKNOWN, context.getNameLocation(node), ISSUE_ENUM_ANNOTATED_UNKNOWN.getBriefDescription(TextFormat.TEXT), fix() .replace() .name("Remove @Json") .range(context.getLocation(jsonAnnotation)) .shortenNames() .text(source) .with("") .autoFix() .build(), ) } } } val jsonNames = mutableMapOf() for ((index, constant) in constants.withIndex()) { if (index == unknownIndex) continue val jsonAnnotation = constant.findAnnotation(FQCN_JSON) validateJsonName(context, constant, jsonAnnotation, jsonNames) val name = constant.name val screamingSnake = name.toScreamingSnakeCase() if (name != screamingSnake) { // Only suggest a new Json annotation if one isn't already present val fixName: String val fixReplacement: String if (jsonAnnotation == null) { fixName = "Add @Json(name = \"$name\") and rename to '$screamingSnake'" fixReplacement = "@$FQCN_JSON(name = \"$name\") $screamingSnake" } else { fixName = "Rename to '$screamingSnake'" fixReplacement = screamingSnake } context.report( ISSUE_ENUM_CASING, context.getNameLocation(constant), ISSUE_ENUM_CASING.getBriefDescription(TextFormat.TEXT), quickfixData = fix() .replace() .name(fixName) .range(context.getNameLocation(constant)) .shortenNames() .text(name) .with(fixReplacement) .autoFix() .build(), ) } } } companion object { private const val FQCN_ADAPTED_BY = "dev.zacsweers.moshix.adapters.AdaptedBy" private const val FQCN_JSON_CLASS = "com.squareup.moshi.JsonClass" private const val FQCN_JSON = "com.squareup.moshi.Json" private const val FQCN_JSON_ADAPTER = "com.squareup.moshi.JsonAdapter" private const val FQCN_JSON_ADAPTER_FACTORY = "com.squareup.moshi.JsonAdapter.Factory" private const val FQCN_JSON_QUALIFIER = "com.squareup.moshi.JsonQualifier" private const val FQCN_TYPE_LABEL = "dev.zacsweers.moshix.sealed.annotations.TypeLabel" private const val FQCN_DEFAULT_OBJECT = "dev.zacsweers.moshix.sealed.annotations.DefaultObject" private const val FQCN_SERIALIZED_NAME = "com.google.gson.annotations.SerializedName" private val REQUIRED_VISIBILITIES = setOf(KtTokens.PUBLIC_KEYWORD, KtTokens.INTERNAL_KEYWORD) // This hint mechanism is solely to help identify nested type arguments in the error message // since we can't easily highlight them in nested generics. We can remove this if we figure // out a good way to do it. private const val HINT = "%HINT%" private fun String.withHint(hint: String?): String { return replace(HINT, hint.orEmpty()) } private fun createIssue( subId: String, briefDescription: String, explanation: String, severity: Severity = Severity.ERROR, ): Issue = Issue.create( "MoshiUsage$subId", briefDescription, explanation, Category.CORRECTNESS, 6, severity, implementation = sourceImplementation(), ) private val ISSUE_MISSING_TYPE_LABEL = createIssue( "MissingTypeLabel", "Sealed Moshi subtypes must be annotated with @TypeLabel or @DefaultObject.", """ moshi-sealed requires sealed subtypes to be annotated with @TypeLabel or @DefaultObject. \ Otherwise, moshi-sealed will fail to compile. """ .trimIndent(), ) private val ISSUE_BLANK_GENERATOR = createIssue( "BlankGenerator", "Don't use blank JsonClass.generator values.", """ The default for JsonClass.generator is "", it's redundant to specify an empty one and an \ error to specify a blank one. """ .trimIndent(), ) private val ISSUE_BLANK_TYPE_LABEL = createIssue( "BlankTypeLabel", "Moshi-sealed requires a type label specified after the 'sealed:' prefix.", "Moshi-sealed requires a type label specified after the 'sealed:' prefix.", ) private val ISSUE_SEALED_MUST_BE_SEALED = createIssue( "SealedMustBeSealed", "Moshi-sealed can only be applied to 'sealed' types.", "Moshi-sealed can only be applied to 'sealed' types.", ) private val ISSUE_GENERATE_ADAPTER_SHOULD_BE_TRUE = createIssue( "GenerateAdapterShouldBeTrue", "JsonClass.generateAdapter must be true in order for Moshi code gen to run.", "JsonClass.generateAdapter must be true in order for Moshi code gen to run.", ) private val ISSUE_PRIVATE_CONSTRUCTOR = createIssue( "PrivateConstructor", "Constructors in Moshi classes cannot be private.", """ Constructors in Moshi classes cannot be private. \ Otherwise Moshi cannot invoke it during decoding. """ .trimIndent(), ) private val ISSUE_PRIVATE_PARAMETER = createIssue( "PrivateConstructorProperty", "Constructor parameter properties in Moshi classes cannot be private.", """ Constructor parameter properties in Moshi classes cannot be private. \ Otherwise these properties will not be visible in serialization. """ .trimIndent(), ) private val ISSUE_TRANSIENT_NEEDS_INIT = createIssue( "TransientNeedsInit", "Transient constructor properties must have default values.", """ Transient constructor property parameters in Moshi classes must have default values. Since \ these parameters do not participate in serialization, Moshi cannot fulfill them otherwise \ during construction. """ .trimIndent(), ) private val ISSUE_INAPPROPRIATE_TYPE_LABEL = createIssue( "InappropriateTypeLabel", "Inappropriate @TypeLabel or @DefaultObject annotation.", """ This class declares a @TypeLabel or @DefaultObject annotation but does not appear to \ subclass a sealed Moshi type. Please remove these annotations or extend the appropriate \ sealed Moshi-serialized class. """ .trimIndent(), ) private val ISSUE_DOUBLE_TYPE_LABEL = createIssue( "DoubleTypeLabel", "Only use one of @TypeLabel or @DefaultObject.", """ Only one of @TypeLabel and @DefaultObject annotations should be present. It is an error to \ declare both! """ .trimIndent(), ) private val ISSUE_GENERIC_SEALED_SUBTYPE = createIssue( "GenericSealedSubtype", "Sealed subtypes used with moshi-sealed cannot be generic.", """ Moshi has no way of conveying generics information to sealed subtypes when we create an \ adapter from the base type. As a result, you should remove generics from this subtype. """ .trimIndent(), ) private val ISSUE_DOUBLE_CLASS_ANNOTATION = createIssue( "DoubleClassAnnotation", "Only use one of @AdaptedBy or @JsonClass.", """ Only one of @AdaptedBy and @JsonClass annotations should be present. It is an error to \ declare both! """ .trimIndent(), ) private val ISSUE_VISIBILITY = createIssue( "ClassVisibility", "@JsonClass-annotated types must be public, package-private, or internal.", """ @JsonClass-annotated types must be public, package-private, or internal. Otherwise, Moshi will not be able to access them from generated adapters. """ .trimIndent(), ) private val ISSUE_PARAM_NEEDS_INIT = createIssue( "ParamNeedsInit", "Constructor non-property parameters in Moshi classes must have default values.", """ Constructor non-property parameters in Moshi classes must have default values. Since these \ parameters do not participate in serialization, Moshi cannot fulfill them otherwise during \ construction. """ .trimIndent(), ) private val ISSUE_BLANK_JSON_NAME = createIssue( "BlankJsonName", "Don't use blank names in `@Json`.", """ Blank names in `@Json`, while technically legal, are likely a programmer error and likely to cause encoding issues. """ .trimIndent(), ) private val ISSUE_REDUNDANT_JSON_NAME = createIssue( "RedundantJsonName", "Json.name with the same value as the property/enum member name is redundant.", """ Redundant Json.name values can make code noisier and harder to read, consider removing it \ or suppress this warning with a commented suppression explaining why it's needed. """ .trimIndent(), severity = Severity.WARNING, ) private val ISSUE_SERIALIZED_NAME = createIssue( "SerializedName", "Use Moshi's @Json rather than Gson's @SerializedName.", """ @SerializedName is specific to Gson and will not work with Moshi. Replace it with Moshi's \ equivalent @Json annotation instead (or remove it if @Json is defined already). """ .trimIndent(), ) private val ISSUE_QUALIFIER_RETENTION = createIssue( "QualifierRetention", "JsonQualifiers must have RUNTIME retention.", """ Moshi uses these annotations at runtime, as such they must be available at runtime. In \ Kotlin, this is the default and you can just remove the Retention annotation. In Java, \ you must specify it explicitly with @Retention(RUNTIME). """ .trimIndent(), ) private val ISSUE_QUALIFIER_TARGET = createIssue( "QualifierTarget", "JsonQualifiers must include FIELD targeting.", """ Moshi code gen stores these annotations on generated adapter fields, as such they must be \ allowed on fields. Please specify it explicitly as @Target(FIELD). """ .trimIndent(), ) private val ISSUE_JSON_SITE_TARGET = createIssue( "RedundantSiteTarget", "Use of site-targets on @Json are redundant.", """ Use of site-targets on @Json are redundant and can be removed. Only one, target-less @Json \ annotation is necessary. """ .trimIndent(), ) private val ISSUE_OBJECT = createIssue( "Object", "Object types cannot be annotated with @JsonClass.", """ Object types cannot be annotated with @JsonClass. The only way they are permitted to \ participate in Moshi serialization is if they are a sealed subtype of a Moshi sealed \ class and annotated with `@TypeLabel` or `@DefaultObject` accordingly. """ .trimIndent(), ) private val ISSUE_USE_DATA = createIssue( "UseData", "Model classes should be immutable data classes.", """ @JsonClass-annotated models should be immutable data classes unless there's a very \ specific reason not to. If you want a custom equals/hashcode/toString impls, make it data \ anyway and override the ones you need. If you want non-property parameter values, consider \ making them `@Transient` properties instead with default values. """ .trimIndent(), ) private val ISSUE_UNSUPPORTED_TYPE = createIssue( "UnsupportedType", "This type cannot be annotated with @JsonClass.", """ Abstract, interface, annotation, and inner class types cannot be annotated with @JsonClass. \ If you intend to decode this with a custom adapter, use @AdaptedBy. """ .trimIndent(), ) private val ISSUE_ADAPTED_BY_REQUIRES_ADAPTER = createIssue( "AdaptedByRequiresAdapter", "@AdaptedBy.adapter must be a JsonAdapter or JsonAdapter.Factory.", """ @AdaptedBy.adapter must be a subclass of JsonAdapter or implement JsonAdapter.Factory. """ .trimIndent(), ) private val ISSUE_ADAPTED_BY_REQUIRES_KEEP = createIssue( "AdaptedByRequiresKeep", "Adapters targeted by @AdaptedBy must have @Keep.", """ Adapters targeted by @AdaptedBy must be annotated with @Keep in order to be reflectively \ looked up at runtime. """ .trimIndent(), ) private val ISSUE_MISSING_PRIMARY = createIssue( "MissingPrimary", "@JsonClass-annotated types must have a primary constructor or be sealed.", """ @JsonClass-annotated types must have a primary constructor or be sealed. Otherwise, they \ either have no serializable properties or all the potentially serializable properties are \ mutable (which is not a case we want!). """ .trimIndent(), ) private val ISSUE_ENUM_PROPERTY_COULD_BE_MOSHI = createIssue( "EnumPropertyCouldBeMoshi", "Consider making enum properties also use Moshi.", """ While we have Gson interop, it's convenient to move enums used by Moshi classes to also \ use Moshi so that you can leverage built-in support for UNKNOWN handling and get lint \ checks for it. Simply add `@JsonClass` to this enum class and the appropriate lint will \ guide you. """ .trimIndent(), Severity.WARNING, ) private val ISSUE_ENUM_PROPERTY_DEFAULT_UNKNOWN = createIssue( "EnumPropertyDefaultUnknown", "Suspicious default value to 'UNKNOWN' for a Moshi enum.", """ The enum type of this property is handled by Moshi. This means it will default to \ 'UNKNOWN' if an unrecognized enum is encountered in decoding. At best, it is redundant to \ default a property to this value. At worst, it can change nullability semantics if the \ enum should actually allow nullable values or null on absence. """ .trimIndent(), ) private val ISSUE_VAR_PROPERTY = createIssue( "VarProperty", "Moshi properties should be immutable.", """ While var properties are technically possible, they should not be used with Moshi classes \ as it can lead to asymmetric encoding and thread-safety issues. Consider making this val. """ .trimIndent(), Severity.WARNING, ) private val ISSUE_SNAKE_CASE = createIssue( "SnakeCase", "Consider using `@Json(name = ...)` rather than direct snake casing.", """ Moshi offers `@Json` annotations to specify names to use in JSON serialization, similar \ to Gson's `@SerializedName`. This can help avoid snake_case properties in source directly. """ .trimIndent(), Severity.WARNING, ) private val ISSUE_DUPLICATE_JSON_NAME = createIssue( "DuplicateJsonName", "Duplicate JSON names are errors as JSON does not allow duplicate keys in objects.", """ Duplicate JSON names are errors as JSON does not allow duplicate keys in objects. """ .trimIndent(), ) private val ISSUE_NON_MOSHI_CLASS_PLATFORM = createIssue( "NonMoshiClassPlatform", "Platform type '$HINT' is not natively supported by Moshi.", """ The property type is a platform type (i.e. from java.*, kotlin.*, android.*). Moshi only \ natively supports a small subset of these (primitives, String, and collection interfaces). \ Otherwise, moshi-gson-interop will hand serialization of this property to Gson, which may \ or may not handle it. This will eventually become an error after GSON is removed. """ .trimIndent(), Severity.WARNING, ) private val ISSUE_ARRAY = createIssue( "Array", "Prefer List over Array.", """ Array types are not supported by Moshi, please use a List instead. Arrays are expensive to \ manage in JSON as we don't know lengths ahead of time and they are a mutable code smell in \ what should be immutable value classes. Otherwise, moshi-gson-interop will hand \ serialization of this property to Gson, which may or may not handle it. This will \ eventually become an error after GSON is removed. """ .trimIndent(), Severity.WARNING, ) private val ISSUE_MUTABLE_COLLECTIONS = createIssue( "MutableCollections", "Use immutable collections rather than mutable versions.", """ While mutable collections are technically possible, they should not be used with Moshi \ classes as it can lead to asymmetric encoding and thread-safety issues. Please make them \ immutable versions instead. """ .trimIndent(), ) private val ISSUE_NON_MOSHI_CLASS_COLLECTION = createIssue( "NonMoshiClassCollection", "Concrete Collection type '$HINT' is not natively supported by Moshi.", """ The property type is concrete Collection type (i.e. ArrayList, HashSet, etc). Moshi only \ natively supports their interface types (List, Set, etc). Consider upcasting to the interface type. Otherwise, moshi-gson-interop will hand serialization of this property to \ Gson, which may or may not handle it. """ .trimIndent(), Severity.INFORMATIONAL, ) private val ISSUE_NON_MOSHI_CLASS_MAP = createIssue( "NonMoshiClassMap", "Concrete Map type '$HINT' is not natively supported by Moshi.", """ The property type is concrete Map type (i.e. LinkedHashMap, HashMap, etc). Moshi only \ natively supports their interface type (Map). Consider upcasting to the interface type. \ Otherwise, moshi-gson-interop will hand serialization of this property to Gson, which may \ or may not handle it. """ .trimIndent(), Severity.INFORMATIONAL, ) private val ISSUE_NON_MOSHI_CLASS_INTERNAL = createIssue( "NonMoshiClassInternal", "Non-Moshi internal type '$HINT' is not natively supported by Moshi.", """ The property type is an internal type (i.e. slack.*) but is not a Moshi class itself. \ moshi-gson-interop will hand serialization of this property to Gson, but consider \ converting this type to Moshi as well to improve runtime performance and consistency. """ .trimIndent(), Severity.INFORMATIONAL, ) private val ISSUE_NON_MOSHI_CLASS_EXTERNAL = createIssue( "NonMoshiClassExternal", "External type '$HINT' is not natively supported by Moshi.", """ The property type is an external type (i.e. not a Slack or built-in type). Moshi will try to serialize these reflectively, which is not something we want. Either write a custom \ adapter and annotating this property with `@AdaptedBy` or exclude/remove this type's use. \ Otherwise, moshi-gson-interop will hand serialization of this property to Gson, which may \ or may not handle it (also with reflection). """ .trimIndent(), ) val ISSUE_ENUM_JSON_CLASS_GENERATED = createIssue( "EnumJsonClassGenerated", "Enums annotated with @JsonClass must not set `generateAdapter` to true.", """ Enums annotated with @JsonClass do not need to set "generateAdapter" to true and should \ set it to false. """ .trimIndent(), ) val ISSUE_ENUM_ANNOTATED_UNKNOWN = createIssue( "EnumAnnotatedUnknown", "UNKNOWN members in @JsonClass-annotated enums should not be annotated with @Json", """ UNKNOWN members in @JsonClass-annotated enums should not be annotated with @Json. These \ members are only used as a fallback and never expected in actual JSON bodies. """ .trimIndent(), ) val ISSUE_ENUM_UNKNOWN = createIssue( "EnumMissingUnknown", "Enums serialized with Moshi must reserve the first member as UNKNOWN.", """ For backward compatibility, enums serialized with Moshi must reserve the first \ member as "UNKNOWN". We will automatically substitute this when encountering \ an unrecognized value for this enum during decoding. """ .trimIndent(), ) val ISSUE_ENUM_JSON_CLASS_MISSING = createIssue( "EnumMissingJsonClass", "Enums serialized with Moshi should be annotated with @JsonClass.", """ This enum appears to use Moshi for serialization. Please also add an @JsonClass \ annotation to it to ensure safe handling with unknown values and R8 optimization. """ .trimIndent(), ) val ISSUE_ENUM_CASING = createIssue( "EnumCasing", "Consider using `@Json(name = ...)` rather than lower casing.", """ Moshi offers `@Json` annotations to specify names to use in JSON serialization, similar \ to Gson's `@SerializedName`. This can help avoid lower-casing enum properties in source \ directly. """ .trimIndent(), severity = Severity.WARNING, ) // Please keep in alphabetical order for readability fun issues(): List = listOf( ISSUE_ADAPTED_BY_REQUIRES_ADAPTER, ISSUE_ADAPTED_BY_REQUIRES_KEEP, ISSUE_ARRAY, ISSUE_BLANK_GENERATOR, ISSUE_BLANK_JSON_NAME, ISSUE_BLANK_TYPE_LABEL, ISSUE_DOUBLE_CLASS_ANNOTATION, ISSUE_DOUBLE_TYPE_LABEL, ISSUE_DUPLICATE_JSON_NAME, ISSUE_ENUM_ANNOTATED_UNKNOWN, ISSUE_ENUM_CASING, ISSUE_ENUM_JSON_CLASS_GENERATED, ISSUE_ENUM_JSON_CLASS_MISSING, ISSUE_ENUM_PROPERTY_COULD_BE_MOSHI, ISSUE_ENUM_PROPERTY_DEFAULT_UNKNOWN, ISSUE_ENUM_UNKNOWN, ISSUE_GENERATE_ADAPTER_SHOULD_BE_TRUE, ISSUE_GENERIC_SEALED_SUBTYPE, ISSUE_INAPPROPRIATE_TYPE_LABEL, ISSUE_JSON_SITE_TARGET, ISSUE_MISSING_PRIMARY, ISSUE_MISSING_TYPE_LABEL, ISSUE_MUTABLE_COLLECTIONS, ISSUE_QUALIFIER_RETENTION, ISSUE_QUALIFIER_TARGET, ISSUE_NON_MOSHI_CLASS_COLLECTION, ISSUE_NON_MOSHI_CLASS_EXTERNAL, ISSUE_NON_MOSHI_CLASS_INTERNAL, ISSUE_NON_MOSHI_CLASS_MAP, ISSUE_NON_MOSHI_CLASS_PLATFORM, ISSUE_OBJECT, ISSUE_PARAM_NEEDS_INIT, ISSUE_PRIVATE_CONSTRUCTOR, ISSUE_PRIVATE_PARAMETER, ISSUE_REDUNDANT_JSON_NAME, ISSUE_SEALED_MUST_BE_SEALED, ISSUE_SERIALIZED_NAME, ISSUE_SNAKE_CASE, ISSUE_TRANSIENT_NEEDS_INIT, ISSUE_UNSUPPORTED_TYPE, ISSUE_USE_DATA, ISSUE_VAR_PROPERTY, ISSUE_VISIBILITY, ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/MustUseNamedParamsDetector.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.isJava import org.jetbrains.kotlin.psi.KtValueArgument import org.jetbrains.kotlin.psi.KtValueArgumentList import org.jetbrains.kotlin.psi.KtValueArgumentName import org.jetbrains.kotlin.psi.psiUtil.getChildOfType import org.jetbrains.uast.UCallExpression import slack.lint.util.sourceImplementation class MustUseNamedParamsDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val method = node.resolve() ?: return // Java doesn't have named parameters. if (isJava(method.language)) return if (method.hasAnnotation("slack.lint.annotations.MustUseNamedParams")) { val areAllNamed = node.sourcePsi!! .getChildOfType()!! .children .filterIsInstance() .all { it.getChildOfType() != null } if (!areAllNamed) { context.report( ISSUE, node, context.getLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT), ) } } } } } companion object { val ISSUE: Issue = Issue.create( "MustUseNamedParams", "Calls to @MustUseNamedParams-annotated methods must name all parameters.", "Calls to @MustUseNamedParams-annotated methods must name all parameters.", Category.CORRECTNESS, 9, Severity.ERROR, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/NonKotlinPairDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import java.util.EnumSet import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.getContainingUClass import org.jetbrains.uast.resolveToUElement import org.jetbrains.uast.util.isConstructorCall /** * Checks to make sure that the code base will use the [kotlin.Pair] instead of any other * alternative Pairs. Pairs might come from other APIs, so this Detector is only concerned about * creating new Pairs. * * Cases that this detector should be warning about from the Java (and Kotlin) perspective. * - new androidx.core.util.Pair() * - androidx.core.util.Pair.create() * - new slack.commons.Pair() * - new android.util.Pair() */ class NonKotlinPairDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> { return listOf(UCallExpression::class.java) } override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { when { node.isConstructorCall() -> { checkForBannedPairType(context, node, node.resolveToUElement()?.getContainingUClass()) } node.methodName == METHOD_NAME_PAIR_CREATE -> { val resolved = node.resolveToUElement() ?: return if (resolved is UMethod && resolved.isStatic) { checkForBannedPairType(context, node, resolved.getContainingUClass()) } } } } } } private fun checkForBannedPairType(context: JavaContext, node: UCallExpression, uClass: UClass?) { if (uClass?.qualifiedName in BANNED_PAIR_TYPES) { val location = context.getLocation(node) val issueToReport = ISSUE_KOTLIN_PAIR_NOT_CREATED context.report( issueToReport, location, issueToReport.getBriefDescription(TextFormat.TEXT), null, ) } } companion object { private const val FQN_KOTLIN_PAIR = "kotlin.Pair" private const val FQN_SLACK_COMMONS_PAIR = "slack.commons.Pair" private const val FQN_ANDROIDX_PAIR = "androidx.core.util.Pair" private const val FQN_ANDROID_DEPRECATED_PAIR = "android.util.Pair" private const val FQN_ANDROID_PAIR = "com.android.utils" private const val METHOD_NAME_PAIR_CREATE = "create" private val BANNED_PAIR_TYPES = listOf( FQN_SLACK_COMMONS_PAIR, FQN_ANDROIDX_PAIR, FQN_ANDROID_DEPRECATED_PAIR, FQN_ANDROID_PAIR, ) /** Scope-set used for detectors which are affected by a single Java source file */ private val JAVA_FILE_AND_TEST_SOURCES_SCOPE: EnumSet = EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) private val ISSUE_KOTLIN_PAIR_NOT_CREATED: Issue = Issue.create( "KotlinPairNotCreated", "Use Kotlin's $FQN_KOTLIN_PAIR instead of other Pair types from other libraries like AndroidX and Slack commons", """ We should consolidate to create and use a single type of Pair in the code base. Kotlin's Pair is preferred as it \ works well with Java and Kotlin. It comes with extension functions that Slack's Pair doesn't offer. """, Category.CORRECTNESS, 6, Severity.WARNING, Implementation(NonKotlinPairDetector::class.java, JAVA_FILE_AND_TEST_SOURCES_SCOPE), ) val issues: List = listOf(ISSUE_KOTLIN_PAIR_NOT_CREATED) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/NotNullOperatorDetector.kt ================================================ // Copyright (C) 2024 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import kotlin.jvm.java import org.jetbrains.uast.UPostfixExpression import org.jetbrains.uast.kotlin.isKotlin import slack.lint.util.sourceImplementation class NotNullOperatorDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UPostfixExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler? { if (!isKotlin(context.uastFile?.lang)) return null return object : UElementHandler() { override fun visitPostfixExpression(node: UPostfixExpression) { if (node.operator.text == "!!") { // Report a warning for the usage of `!!` context.report( ISSUE, node, context.getLocation(node.operatorIdentifier ?: node), "Avoid using the `!!` operator", ) } } } } companion object { val ISSUE: Issue = Issue.create( "AvoidUsingNotNullOperator", "Avoid using the !! operator in Kotlin", """ The `!!` operator is a not-null assertion in Kotlin that will lead to a \ `NullPointerException` if the value is null. It's better to use safe \ null-handling mechanisms like `?.`, `?:`, `?.let`, etc. """, Category.CORRECTNESS, 6, Severity.WARNING, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/NullableConcurrentHashMapDetector.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Location import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiNewExpression import com.intellij.psi.PsiReferenceParameterList import com.intellij.psi.PsiTypeElement import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtNullableType import org.jetbrains.kotlin.psi.KtTypeReference import org.jetbrains.kotlin.psi.psiUtil.getChildOfType import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UVariable import org.jetbrains.uast.getContainingUClass import org.jetbrains.uast.resolveToUElement import org.jetbrains.uast.util.isConstructorCall import slack.lint.util.sourceImplementation class NullableConcurrentHashMapDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java, UVariable::class.java) override fun createUastHandler(context: JavaContext) = object : UElementHandler() { override fun visitVariable(node: UVariable) { if (node.typeReference?.getQualifiedName() != CONCURRENT_HASH_MAP) return // Check if key type is nullable node.check(0, "key") // Check if value type is nullable node.check(1, "value") } private fun UVariable.check(typeArgIndex: Int, name: String) { val location = when (val typeRefPsi = typeReference?.sourcePsi) { is KtTypeReference -> { typeRefPsi.typeElement ?.typeArgumentsAsTypes ?.takeIf { it.size == 2 } ?.getOrNull(typeArgIndex) ?.typeElement ?.takeIf { it is KtNullableType } ?.let(context::getLocation) } is PsiTypeElement -> { typeRefPsi.innermostComponentReferenceElement ?.getChildOfType() ?.takeIf { it.typeParameterElements.size == 2 } ?.obtainIfNullable(typeArgIndex) } else -> null } if (location != null) { context.report(ISSUE, location, "ConcurrentHashMap should not use nullable $name types") } } override fun visitCallExpression(node: UCallExpression) { // Check if it's a constructor call if (!node.isConstructorCall()) return // Get the class being constructed val uClass = node.resolveToUElement()?.getContainingUClass() ?: return // Check if it's a ConcurrentHashMap if (uClass.qualifiedName != CONCURRENT_HASH_MAP) return // Check if key type is nullable node.check(0, "key") // Check if value type is nullable node.check(1, "value") } private fun UCallExpression.check(typeArgIndex: Int, name: String) { val location = when (val sourcePsi = sourcePsi) { is KtCallExpression -> { val typeElement = sourcePsi.typeArguments.getOrNull(typeArgIndex)?.typeReference?.typeElement ?: return val isNullable = typeElement is KtNullableType if (isNullable) { context.getLocation(typeElement) } else { null } } is PsiNewExpression -> { sourcePsi.classReference ?.getChildOfType() ?.takeIf { it.typeParameterElements.size == 2 } ?.obtainIfNullable(typeArgIndex) } else -> null } if (location != null) { context.report(ISSUE, location, "ConcurrentHashMap should not use nullable $name types") } } private fun PsiReferenceParameterList.obtainIfNullable(typeArgIndex: Int): Location? { val typeArg = typeParameterElements.getOrNull(typeArgIndex) ?: return null val isNullable = typeArg.type.annotations.any { it.qualifiedName?.endsWith(".Nullable") == true } return if (isNullable) { context.getLocation(typeArg) } else { null } } } companion object { private const val CONCURRENT_HASH_MAP = "java.util.concurrent.ConcurrentHashMap" val ISSUE = Issue.create( id = "NullableConcurrentHashMap", briefDescription = "ConcurrentHashMap should not use nullable types", explanation = """ ConcurrentHashMap does not support null keys or values. \ Use non-nullable types for both keys and values when creating a ConcurrentHashMap. """, category = Category.CORRECTNESS, priority = 5, severity = Severity.ERROR, implementation = sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/RawDispatchersUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import java.util.EnumSet import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UCallableReferenceExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.kotlin.isKotlin /** * This is a [Detector] for detecting direct usages of Kotlin coroutines' * [kotlinx.coroutines.Dispatchers] properties, which we want to prevent in favor of our * `SlackDispatchers` abstraction. */ class RawDispatchersUsageDetector : Detector(), SourceCodeScanner { companion object { private val SCOPES = Implementation(RawDispatchersUsageDetector::class.java, EnumSet.of(Scope.JAVA_FILE)) val ISSUE: Issue = Issue.create( "RawDispatchersUse", "Use SlackDispatchers.", """ Direct use of `Dispatchers.*` APIs are discouraged as they are difficult to test. Prefer using \ `SlackDispatchers`. """, Category.CORRECTNESS, 6, Severity.ERROR, SCOPES, ) private const val DISPATCHERS_CLASS = "kotlinx.coroutines.Dispatchers" private val PROPERTY_GETTERS = setOf( "getDefault", "getIO", "getMain", "getUnconfined", "Default", "IO", "Main", "Unconfined", ) } override fun getApplicableUastTypes(): List> = listOf( UCallExpression::class.java, UCallableReferenceExpression::class.java, UQualifiedReferenceExpression::class.java, ) override fun createUastHandler(context: JavaContext): UElementHandler? { // Only applicable on Kotlin files if (!isKotlin(context.uastFile?.lang)) return null fun report(node: UElement) { context.report(ISSUE, context.getLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT)) } fun String?.isDispatcherGetter(): Boolean { return this in PROPERTY_GETTERS } fun PsiClass?.isDispatchersClass(): Boolean { if (this == null) return false return qualifiedName == DISPATCHERS_CLASS } return object : UElementHandler() { // Awkward but this is how to look for property getter calls override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) { val expressionType = node.receiver.getExpressionType() ?: return if (expressionType is PsiClassType && expressionType.resolve().isDispatchersClass()) { val uDecl = node.selector.tryResolveUDeclaration() ?: return if (uDecl is UMethod && uDecl.name.isDispatcherGetter()) { report(node) } } } override fun visitCallExpression(node: UCallExpression) { if (node.methodName.isDispatcherGetter()) { val resolved = node.resolve() ?: return if (resolved.containingClass.isDispatchersClass()) { report(node) } } } override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) { if (node.callableName.isDispatcherGetter()) { val qualifierType = node.qualifierType ?: return if (qualifierType is PsiClassType && qualifierType.resolve().isDispatchersClass()) report(node) } } } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/RedactedUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UField import org.jetbrains.uast.UMethod import org.jetbrains.uast.kotlin.isKotlin import slack.lint.util.sourceImplementation /** * A simple detector that ensures that `@Redacted` annotations are only used in Kotlin files (Java * is unsupported). */ // TODO check for toString() impls in Kotlin classes using Redacted? i.e. it's an error to have both // TODO check that redacted classes are data classes class RedactedUsageDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> { return listOf(UClass::class.java, UMethod::class.java, UField::class.java) } override fun createUastHandler(context: JavaContext): UElementHandler? { // Redacted can only be used in Kotlin files, so this check only checks in Java files if (isKotlin(context.uastFile?.lang)) return null return object : UElementHandler() { override fun visitClass(node: UClass) = node.checkRedacted() override fun visitField(node: UField) = node.checkRedacted() override fun visitMethod(node: UMethod) = node.checkRedacted() fun UAnnotated.checkRedacted() { uAnnotations .find { it.qualifiedName?.contains(NAME_REDACTED, ignoreCase = true) == true } ?.let { redactedAnnotation -> context.report( JAVA_USAGE, context.getLocation(redactedAnnotation), JAVA_USAGE.getBriefDescription(TextFormat.TEXT), quickfixData = null, ) } } } } companion object { // We check simple name only rather than any specific redacted annotation private const val NAME_REDACTED = "redacted" private val JAVA_USAGE: Issue = Issue.create( "RedactedInJavaUsage", "@Redacted is only supported in Kotlin classes!", "@Redacted is only supported in Kotlin classes!", Category.CORRECTNESS, 9, Severity.ERROR, sourceImplementation(), ) val ISSUES = listOf(JAVA_USAGE) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/RestrictCallsToDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UFile import org.jetbrains.uast.UMethod import org.jetbrains.uast.getContainingUFile import org.jetbrains.uast.resolveToUElement import org.jetbrains.uast.toUElementOfType import slack.lint.util.sourceImplementation class RestrictCallsToDetector : Detector(), SourceCodeScanner { companion object { val ISSUE: Issue = Issue.create( "RestrictCallsTo", "Methods annotated with @RestrictedCallsTo should only be called from the specified scope.", """ This method is intended to only be called from the specified scope despite it being \ public. This could be due to its use in an interface or similar. Overrides are still \ ok. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private const val RESTRICT_CALLS_TO_ANNOTATION = "slack.lint.annotations.RestrictCallsTo" } override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext) = object : UElementHandler() { @Suppress("UNUSED_VARIABLE") // Toe-hold for now until we support other scopes override fun visitCallExpression(node: UCallExpression) { val method = node.resolveToUElement() as? UMethod ?: return val (restrictCallsTo, annotatedMethod) = method .superMethodSequence() .mapNotNull { superMethod -> superMethod.findAnnotation(RESTRICT_CALLS_TO_ANNOTATION)?.let { it to superMethod } } .firstOrNull() ?: return val containingFile = annotatedMethod.getContainingUFile() ?: return val callingFile = node.getContainingUFile() ?: return if (!callingFile.isSameAs(containingFile)) { context.report( ISSUE, node, context.getLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT), ) } } private fun UMethod.superMethodSequence(): Sequence { return generateSequence(this) { if (context.evaluator.isOverride(it)) { // Note this doesn't try to check multiple interfaces, but we can fix that in the future // if it matters it.findSuperMethods()[0].toUElementOfType() } else { null } } } } // UFile isn't inherently comparable, so package and simple name are close enough for us. private fun UFile.isSameAs(other: UFile): Boolean { return this.packageName == other.packageName && this.sourcePsi.name == other.sourcePsi.name } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/SerializableDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.uast.UClass import slack.lint.util.implements import slack.lint.util.sourceImplementation class SerializableDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UClass::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitClass(node: UClass) { if (node.isEnum) return val implementsSerializable = node.implements("java.io.Serializable") { fqcn -> // Only look in slack sources "slack" in fqcn } if (implementsSerializable) { // TODO after we drop Serializable entirely, we should always make it an error. if (node.implements("android.os.Parcelable")) return context.report( ISSUE, context.getNameLocation(node), ISSUE.getBriefDescription(TextFormat.TEXT), ) } } } } companion object { val ISSUE: Issue = Issue.create( "SerializableUsage", "Don't use Serializable.", """ Don't use Serializable. It's brittle, requires reflection, does not \ work well with Kotlin, and prevents us from using Core Library Desugaring. \ Either implement Parcelable too or use another safer serialization mechanism. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/SlackIssueRegistry.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.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 import slack.lint.denylistedapis.DenyListedApiDetector import slack.lint.eithernet.DoNotExposeEitherNetInRepositoriesDetector import slack.lint.inclusive.InclusiveNamingChecker import slack.lint.mocking.ErrorProneDoNotMockDetector import slack.lint.mocking.MockDetector import slack.lint.parcel.ParcelizeFunctionPropertyDetector import slack.lint.resources.FullyQualifiedResourceDetector import slack.lint.resources.MissingResourceImportAliasDetector import slack.lint.resources.WrongResourceImportAliasDetector import slack.lint.retrofit.RetrofitUsageDetector import slack.lint.rx.RxObservableEmitDetector import slack.lint.rx.RxSubscribeOnMainDetector import slack.lint.text.SpanMarkPointMissingMaskDetector import slack.lint.ui.DoNotCallViewToString import slack.lint.ui.ItemDecorationViewBindingDetector @AutoService(IssueRegistry::class) class SlackIssueRegistry : IssueRegistry() { override val vendor: Vendor = Vendor( vendorName = "slack", identifier = "slack-lint", feedbackUrl = "https://github.com/slackhq/slack-lints", contact = "https://github.com/slackhq/slack-lints", ) override val api: Int = CURRENT_API override val minApi: Int = CURRENT_API override val issues: List = buildList { addAll(ViewContextDetector.issues) addAll(ArgInFormattedQuantityStringResDetector.issues) addAll(DaggerIssuesDetector.ISSUES) addAll(NonKotlinPairDetector.issues) add(DoNotCallProvidersDetector.ISSUE) addAll(InclusiveNamingChecker.ISSUES) add(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) add(DeprecatedSqlUsageDetector.ISSUE) add(JavaOnlyDetector.ISSUE) add(SerializableDetector.ISSUE) add(RawDispatchersUsageDetector.ISSUE) add(MainScopeUsageDetector.ISSUE) add(RxSubscribeOnMainDetector.ISSUE) addAll(RxObservableEmitDetector.issues) addAll(GuavaPreconditionsDetector.issues) addAll(MockDetector.ALL_ISSUES) add(ErrorProneDoNotMockDetector.ISSUE) addAll(MoshiUsageDetector.issues()) addAll(FragmentDaggerFieldInjectionDetector.issues) addAll(RedactedUsageDetector.ISSUES) add(InjectInJavaDetector.ISSUE) add(RetrofitUsageDetector.ISSUE) add(RestrictCallsToDetector.ISSUE) add(SpanMarkPointMissingMaskDetector.ISSUE) add(DoNotExposeEitherNetInRepositoriesDetector.ISSUE) add(FullyQualifiedResourceDetector.ISSUE) add(MissingResourceImportAliasDetector.ISSUE) add(WrongResourceImportAliasDetector.ISSUE) addAll(DenyListedApiDetector.ISSUES) add(ParcelizeFunctionPropertyDetector.ISSUE) add(ExceptionMessageDetector.ISSUE) add(TestParameterSiteTargetDetector.ISSUE) add(MustUseNamedParamsDetector.ISSUE) add(NotNullOperatorDetector.ISSUE) add(DoNotCallViewToString.ISSUE) add(ItemDecorationViewBindingDetector.ISSUE) add(NullableConcurrentHashMapDetector.ISSUE) add(CircuitScreenDataClassDetector.ISSUE) add(JsonInflaterMoshiCompatibilityDetector.ISSUE) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/TestParameterSiteTargetDetector.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.UParameter import org.jetbrains.uast.kotlin.isKotlin import org.jetbrains.uast.toUElementOfType import slack.lint.util.sourceImplementation /** * Detector that checks for `@TestParameter` annotations on parameter properties and ensures they * have param: site targets. */ class TestParameterSiteTargetDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> { return listOf(UMethod::class.java) } override fun createUastHandler(context: JavaContext): UElementHandler? { // Only check Kotlin files if (!isKotlin(context.uastFile?.lang)) return null return object : UElementHandler() { override fun visitMethod(node: UMethod) { if (!node.isConstructor) return // Check parameters for (parameter in node.uastParameters) { checkParameter(parameter) } } private fun checkParameter(parameter: UParameter) { val sourcePsi = parameter.sourcePsi as? KtParameter ?: return // Only check property parameters if (!sourcePsi.isPropertyParameter()) return // Find @TestParameter annotations for (annotationEntry in sourcePsi.annotationEntries) { val qualifiedName = (annotationEntry.toUElementOfType())?.qualifiedName // Check if this is a TestParameter annotation if ( qualifiedName == TEST_PARAMETER_ANNOTATION || annotationEntry.shortName?.asString() == "TestParameter" ) { // Check if the annotation has a param: site target if (annotationEntry.useSiteTarget?.text != "param") { // Get the full text of the annotation val annotationText = annotationEntry.text // Create the replacement text by adding or replacing the site target val replacementText = if (annotationEntry.useSiteTarget != null) { // Replace existing site target with param: annotationText.replace("${annotationEntry.useSiteTarget!!.text}:", "param:") } else { // Add param: site target annotationText.replace("@", "@param:") } context.report( ISSUE, context.getLocation(annotationEntry), ISSUE.getBriefDescription(TextFormat.TEXT), LintFix.create().replace().text(annotationText).with(replacementText).build(), ) } } } } } } companion object { private const val TEST_PARAMETER_ANNOTATION = "com.google.testing.junit.testparameterinjector.TestParameter" val ISSUE = Issue.create( id = "TestParameterSiteTarget", briefDescription = "`TestParameter` annotation has the wrong site target", explanation = """ `TestParameter` annotations on parameter properties must have `param:` site targets.\ \ For example:\ ```kotlin\ class MyTest(\ @param:TestParameter val myParam: String\ )\ ```\ \ For more information, see: https://github.com/google/TestParameterInjector/issues/49 """, category = Category.CORRECTNESS, priority = 5, severity = Severity.ERROR, implementation = sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/ViewContextDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.intellij.psi.PsiMethod import com.intellij.psi.impl.compiled.ClsClassImpl import org.jetbrains.uast.UBinaryExpressionWithType import org.jetbrains.uast.UCallExpression import slack.lint.util.sourceImplementation /** * This [Detector] scans Java files for calls to methods named "getContext". It then checks if the * object being called is an `android.view.View` and if they are casting the returned * `android.content.Context` to `android.app.Activity`. If so, an [Issue] with id * `CastingViewContextToActivity` is reported. */ @Suppress("UnstableApiUsage") class ViewContextDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List { return listOf("getContext") } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { if ( method.parent is ClsClassImpl && "android.view.View" == (method.parent as ClsClassImpl).qualifiedName ) { // Check if the parent expression is a cast expression var parent = node.uastParent if (parent != null) { // Check if it's called as 'view.getContext()' if (parent.uastParent is UBinaryExpressionWithType) { parent = parent.uastParent } if (parent is UBinaryExpressionWithType) { // See if it is being cast to android.App.Activity if ("android.app.Activity" == parent.type.canonicalText) { // report an issue context.report( issue = ISSUE_VIEW_CONTEXT_CAST, scope = node, location = context.getLocation(parent), message = ISSUE_VIEW_CONTEXT_CAST.getBriefDescription(TextFormat.TEXT), ) } } } } } companion object { val ISSUE_VIEW_CONTEXT_CAST: Issue = Issue.create( "CastingViewContextToActivity", "Unsafe cast of `Context` to `Activity`", """`View.getContext()` is not guaranteed to return an `Activity` and can often \ return a `ContextWrapper` instead resulting in a `ClassCastException`. Instead, use \ `UiUtils.getActivityFromView()`. """, CORRECTNESS, 9, Severity.ERROR, sourceImplementation(), ) val issues: List get() = listOf(ISSUE_VIEW_CONTEXT_CAST) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/denylistedapis/DenyListedApiDetector.kt ================================================ /* Copyright 2022 Square, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package slack.lint.denylistedapis import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.LocationType.NAME import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity.ERROR import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.XmlContext import com.android.tools.lint.detector.api.XmlScanner import com.intellij.psi.PsiField import com.intellij.psi.PsiMethod import java.util.EnumSet import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.UImportStatement import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.util.isConstructorCall import org.w3c.dom.Element import slack.lint.denylistedapis.DenyListedEntry.Companion.MatchAll /** * Deny-listed APIs that we don't want people to use. * * Adapted from https://gist.github.com/JakeWharton/1f102d98cd10133b03a5f374540c327a */ internal class DenyListedApiDetector : Detector(), SourceCodeScanner, XmlScanner { override fun getApplicableUastTypes() = CONFIG.applicableTypes() override fun createUastHandler(context: JavaContext) = CONFIG.visitor(context) override fun getApplicableElements() = CONFIG.applicableLayoutInflaterElements.keys override fun visitElement(context: XmlContext, element: Element) = CONFIG.visitor(context, element) private class DenyListConfig(vararg entries: DenyListedEntry) { private class TypeConfig(entries: List) { @Suppress("UNCHECKED_CAST") // Safe because of filter call. val functionEntries = entries.groupBy { it.functionName }.filterKeys { it != null } as Map> @Suppress("UNCHECKED_CAST") // Safe because of filter call. val referenceEntries = entries.groupBy { it.fieldName }.filterKeys { it != null } as Map> } val issues = entries.asSequence().map { it.issue }.distinctBy { it.id }.toList() private val typeConfigs = entries.groupBy { it.className }.mapValues { (_, entries) -> TypeConfig(entries) } val applicableLayoutInflaterElements = entries .filter { it.functionName == "" } .filter { it.arguments == null || it.arguments == listOf("android.content.Context", "android.util.AttributeSet") } .groupBy { it.className } .mapValues { (cls, entries) -> entries.singleOrNull() ?: error("Multiple two-arg init rules for $cls") } fun applicableTypes() = listOf>( UCallExpression::class.java, UImportStatement::class.java, UQualifiedReferenceExpression::class.java, ) fun visitor(context: JavaContext) = object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val function = node.resolve() ?: return val className = function.containingClass?.qualifiedName val typeConfig = typeConfigs[className] ?: return val functionName = if (node.isConstructorCall()) { "" } else { // Kotlin compiler mangles function names that use inline value types as parameters by // suffixing them // with a hyphen. // https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md#mangling-rules function.name.substringBefore("-") } val deniedFunctions = typeConfig.functionEntries.getOrDefault(functionName, emptyList()) + typeConfig.functionEntries.getOrDefault(MatchAll, emptyList()) deniedFunctions.forEach { denyListEntry -> if (denyListEntry.allowInTests && context.isTestSource) { return@forEach } else if ( denyListEntry.parametersMatchWith(function) && denyListEntry.argumentsMatchWith(node) ) { context.report( issue = denyListEntry.issue, location = context.getNameLocation(node), message = denyListEntry.errorMessage, ) } } } override fun visitImportStatement(node: UImportStatement) { val reference = node.resolve() as? PsiField ?: return visitField(reference, node) } override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) { val reference = node.resolve() as? PsiField ?: return visitField(reference, node) } private fun visitField(reference: PsiField, node: UElement) { val className = reference.containingClass?.qualifiedName val typeConfig = typeConfigs[className] ?: return val referenceName = reference.name val deniedFunctions = typeConfig.referenceEntries.getOrDefault(referenceName, emptyList()) + typeConfig.referenceEntries.getOrDefault(MatchAll, emptyList()) deniedFunctions.forEach { denyListEntry -> if (denyListEntry.allowInTests && context.isTestSource) { return@forEach } context.report( issue = denyListEntry.issue, location = context.getLocation(node), message = denyListEntry.errorMessage, ) } } } fun visitor(context: XmlContext, element: Element) { val denyListEntry = applicableLayoutInflaterElements.getValue(element.tagName) context.report( issue = denyListEntry.issue, location = context.getLocation(element, type = NAME), message = denyListEntry.errorMessage, ) } private fun DenyListedEntry.parametersMatchWith(function: PsiMethod): Boolean { val expected = parameters val actual = function.parameterList.parameters.map { it.type.canonicalText } return when { expected == null -> true expected.isEmpty() && actual.isEmpty() -> true expected.size != actual.size -> false else -> expected == actual } } private fun DenyListedEntry.argumentsMatchWith(node: UCallExpression): Boolean { // "arguments" being null means we don't care about this check and it should just return true. val expected = arguments ?: return true val actual = node.valueArguments return when { expected.size != actual.size -> false else -> expected.zip(actual).all { (expectedValue, actualValue) -> argumentMatches(expectedValue, actualValue) } } } private fun argumentMatches(expectedValue: String, actualValue: UExpression): Boolean { if (expectedValue == "*") return true val renderString = (actualValue as? ULiteralExpression)?.asRenderString() ?: (actualValue as? UQualifiedReferenceExpression) ?.asRenderString() // Helps to match against static method params // 'Class.staticMethod()'. if (expectedValue == renderString) return true return false } } companion object { val DEFAULT_ISSUE = createIssue("DenyListedApi") val BLOCKING_ISSUE = createIssue("DenyListedBlockingApi") private val CONFIG = DenyListConfig( DenyListedEntry( className = "io.reactivex.rxjava3.core.Observable", functionName = "hide", errorMessage = "There should be no reason to defend against downcasting an Observable to " + "an implementation type like Relay or Subject in a closed codebase. Doing this incurs " + "needless runtime memory and performance overhead. Relays and Subjects both extend from " + "Observable and can be supplied to functions accepting Observable directly. When " + "returning a Relay or Subject, declare the return type explicitly as Observable " + "(e.g., fun foo(): Observable = fooRelay).", ), DenyListedEntry( className = "io.reactivex.rxjava3.core.Flowable", functionName = "hide", errorMessage = "There should be no reason to defend against downcasting an Flowable to " + "an implementation type like FlowableProcessor in a closed codebase. Doing this incurs " + "needless runtime memory and performance overhead. FlowableProcessor extends from " + "Flowable and can be supplied to functions accepting Flowable directly. When " + "returning a FlowableProcessor, declare the return type explicitly as Flowable " + "(e.g., fun foo(): Flowable = fooProcessor).", ), DenyListedEntry( className = "io.reactivex.rxjava3.core.Completable", functionName = "hide", errorMessage = "There should be no reason to defend against downcasting a Completable to " + "an implementation type like CompletableSubject in a closed codebase. Doing this incurs " + "needless runtime memory and performance overhead. CompletableSubject extends from " + "Completable and can be supplied to functions accepting Completable directly. When " + "returning a CompletableSubject, declare the return type explicitly as Completable " + "(e.g., fun foo(): Completable = fooSubject).", ), DenyListedEntry( className = "io.reactivex.rxjava3.core.Maybe", functionName = "hide", errorMessage = "There should be no reason to defend against downcasting a Maybe to " + "an implementation type like MaybeSubject in a closed codebase. Doing this incurs " + "needless runtime memory and performance overhead. MaybeSubject extends from " + "Maybe and can be supplied to functions accepting Maybe directly. When " + "returning a MaybeSubject, declare the return type explicitly as Maybe " + "(e.g., fun foo(): Maybe = fooSubject).", ), DenyListedEntry( className = "io.reactivex.rxjava3.core.Single", functionName = "hide", errorMessage = "There should be no reason to defend against downcasting a Single to " + "an implementation type like SingleSubject in a closed codebase. Doing this incurs " + "needless runtime memory and performance overhead. SingleSubject extends from " + "Single and can be supplied to functions accepting Single directly. When " + "returning a SingleSubject, declare the return type explicitly as Single " + "(e.g., fun foo(): Single = fooSubject).", ), DenyListedEntry( className = "androidx.core.content.ContextCompat", functionName = "getDrawable", parameters = listOf("android.content.Context", "int"), errorMessage = "Use Context#getDrawableCompat() instead", ), DenyListedEntry( className = "androidx.core.content.res.ResourcesCompat", functionName = "getDrawable", parameters = listOf("android.content.Context", "int"), errorMessage = "Use Context#getDrawableCompat() instead", ), DenyListedEntry( className = "android.support.test.espresso.matcher.ViewMatchers", functionName = "withId", parameters = listOf("int"), errorMessage = "Consider matching the content description instead. IDs are " + "implementation details of how a screen is built, not how it works. You can't" + " tell a user to click on the button with ID 428194727 so our tests should not" + " be doing that. ", ), DenyListedEntry( className = "android.view.View", functionName = "setOnClickListener", parameters = listOf("android.view.View.OnClickListener"), arguments = listOf("null"), errorMessage = "This fails to also set View#isClickable. Use View#clearOnClickListener() instead", ), DenyListedEntry( // If you are deny listing an extension method you need to ascertain the fully qualified // name // of the class the extension method ends up on. className = "kotlinx.coroutines.flow.FlowKt__CollectKt", functionName = "launchIn", errorMessage = "Use the structured concurrent CoroutineScope#launch and Flow#collect " + "APIs instead of reactive Flow#onEach and Flow#launchIn. Suspend calls like Flow#collect " + "can be refactored into standalone suspend funs and mixed in with regular control flow " + "in a suspend context, but calls that invoke CoroutineScope#launch and Flow#collect at " + "the same time hide the suspend context, encouraging the developer to continue working in " + "the reactive domain.", ), DenyListedEntry( className = "androidx.viewpager2.widget.ViewPager2", functionName = "setId", parameters = listOf("int"), arguments = listOf("ViewCompat.generateViewId()"), errorMessage = "Use an id defined in resources or a statically created instead of generating with ViewCompat.generateViewId(). See https://issuetracker.google.com/issues/185820237", ), DenyListedEntry( className = "androidx.viewpager2.widget.ViewPager2", functionName = "setId", parameters = listOf("int"), arguments = listOf("View.generateViewId()"), errorMessage = "Use an id defined in resources or a statically created instead of generating with View.generateViewId(). See https://issuetracker.google.com/issues/185820237", ), DenyListedEntry( className = "java.util.LinkedList", functionName = "", errorMessage = "For a stack/queue/double-ended queue use ArrayDeque, for a list use ArrayList. Both are more efficient internally.", ), DenyListedEntry( className = "java.util.Stack", functionName = "", errorMessage = "For a stack use ArrayDeque which is more efficient internally.", ), DenyListedEntry( className = "java.util.Vector", functionName = "", errorMessage = "For a vector use ArrayList or ArrayDeque which are more efficient internally.", ), DenyListedEntry( className = "io.reactivex.rxjava3.schedulers.Schedulers", functionName = "newThread", errorMessage = "Use a scheduler which wraps a cached set of threads. There should be no reason to be arbitrarily creating threads on Android.", ), // TODO this would conflict with MagicNumber in detekt, revisit // DenyListedEntry( // className = "android.os.Build.VERSION_CODES", // fieldName = MatchAll, // errorMessage = // "No one remembers what these constants map to. Use the API level integer value // directly since it's self-defining." // ), // TODO we should do this too, but don't currently. // DenyListedEntry( // className = "java.time.Instant", // functionName = "now", // errorMessage = "Use com.squareup.cash.util.Clock to get the time." // ), DenyListedEntry( className = "kotlinx.coroutines.rx3.RxCompletableKt", functionName = "rxCompletable", errorMessage = "rxCompletable defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic.", parameters = listOf( "kotlin.coroutines.CoroutineContext", "kotlin.jvm.functions.Function2,? extends java.lang.Object>", ), arguments = listOf("*"), ), DenyListedEntry( className = "kotlinx.coroutines.rx3.RxMaybeKt", functionName = "rxMaybe", errorMessage = "rxMaybe defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic.", parameters = listOf( "kotlin.coroutines.CoroutineContext", "kotlin.jvm.functions.Function2,? extends java.lang.Object>", ), arguments = listOf("*"), ), DenyListedEntry( className = "kotlinx.coroutines.rx3.RxSingleKt", functionName = "rxSingle", errorMessage = "rxSingle defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic.", parameters = listOf( "kotlin.coroutines.CoroutineContext", "kotlin.jvm.functions.Function2,? extends java.lang.Object>", ), arguments = listOf("*"), ), DenyListedEntry( className = "kotlinx.coroutines.rx3.RxObservableKt", functionName = "rxObservable", errorMessage = "rxObservable defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic.", parameters = listOf( "kotlin.coroutines.CoroutineContext", "kotlin.jvm.functions.Function2,? super kotlin.coroutines.Continuation,? extends java.lang.Object>", ), arguments = listOf("*"), ), DenyListedEntry( className = "java.util.Date", functionName = MatchAll, errorMessage = "Use java.time.Instant or java.time.ZonedDateTime instead. There is no reason to use java.util.Date in Java 8+.", ), DenyListedEntry( className = "java.util.Calendar", fieldName = MatchAll, errorMessage = "Use java.time.Instant or java.time.ZonedDateTime instead. There is no reason to use java.util.Calendar in Java 8+.", ), DenyListedEntry( className = "java.util.Calendar", functionName = MatchAll, errorMessage = "Use java.time.Instant or java.time.ZonedDateTime instead. There is no reason to use java.util.Calendar in Java 8+.", ), DenyListedEntry( className = "java.text.DateFormat", fieldName = MatchAll, errorMessage = "Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+.", ), DenyListedEntry( className = "java.text.SimpleDateFormat", fieldName = MatchAll, errorMessage = "Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+.", ), DenyListedEntry( className = "java.text.DateFormat", functionName = MatchAll, errorMessage = "Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+.", ), DenyListedEntry( className = "java.text.SimpleDateFormat", functionName = MatchAll, errorMessage = "Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+.", ), DenyListedEntry( className = "kotlin.ResultKt", functionName = "runCatching", errorMessage = "runCatching has hidden issues when used with coroutines as it catches and doesn't rethrow CancellationException. " + "This can interfere with coroutines cancellation handling! " + "Prefer catching specific exceptions based on the current case.", ), // Blocking calls DenyListedEntry( className = "kotlinx.coroutines.BuildersKt", functionName = "runBlocking", errorMessage = "Blocking calls in coroutines can cause deadlocks and application jank. " + "Prefer making the enclosing function a suspend function or refactoring this in a way to use non-blocking calls. " + "If running in a test, use runTest {} or Turbine to test synchronous values.", issue = BLOCKING_ISSUE, ), *rxJavaBlockingCalls().toTypedArray(), ) val ISSUES = CONFIG.issues private fun createIssue( id: String, briefDescription: String = "Deny-listed API", explanation: String = "This lint check flags usages of APIs in external libraries that we prefer not to use.", ): Issue { return Issue.create( id = id, briefDescription = briefDescription, explanation = explanation, category = CORRECTNESS, priority = 5, severity = ERROR, implementation = Implementation( DenyListedApiDetector::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.RESOURCE_FILE, Scope.TEST_SOURCES), EnumSet.of(Scope.JAVA_FILE), EnumSet.of(Scope.RESOURCE_FILE), EnumSet.of(Scope.TEST_SOURCES), ), ) } } } data class DenyListedEntry( val className: String, /** The function name to match, [MatchAll] to match all functions, or null if matching a field. */ val functionName: String? = null, /** The field name to match, [MatchAll] to match all fields, or null if matching a function. */ val fieldName: String? = null, /** Fully-qualified types of function parameters to match, or null to match all overloads. */ val parameters: List? = null, /** Argument expressions to match at the call site, or null to match all invocations. */ val arguments: List? = null, val errorMessage: String, /** * Option to allow this issue in tests. Should _only_ be reserved for invocations that make sense * in tests. */ val allowInTests: Boolean = false, /** * Issue that should be reported for this entry. Defaults to [DenyListedApiDetector.DEFAULT_ISSUE] */ val issue: Issue = DenyListedApiDetector.DEFAULT_ISSUE, ) { init { require((functionName == null) xor (fieldName == null)) { "One of functionName or fieldName must be set" } } companion object { const val MatchAll = "*" } } private fun rxJavaBlockingCalls() = listOf( "io.reactivex.rxjava3.core.Completable" to listOf("blockingAwait"), "io.reactivex.rxjava3.core.Single" to listOf("blockingGet", "blockingSubscribe"), "io.reactivex.rxjava3.core.Maybe" to listOf("blockingGet", "blockingSubscribe"), "io.reactivex.rxjava3.core.Observable" to listOf( "blockingFirst", "blockingForEach", "blockingIterable", "blockingLatest", "blockingMostRecent", "blockingNext", "blockingSingle", "blockingSubscribe", ), "io.reactivex.rxjava3.core.Flowable" to listOf( "blockingFirst", "blockingForEach", "blockingIterable", "blockingLatest", "blockingMostRecent", "blockingNext", "blockingSingle", "blockingSubscribe", ), ) .flatMap { (className, methods) -> val shortType = className.substringAfterLast('.') val isCompletable = shortType == "Completable" val orMessage = if (!isCompletable) { " Completable (if you want to hide emission values but defer subscription)," } else { "" } methods.map { method -> DenyListedEntry( className = className, functionName = method, errorMessage = "Blocking calls in RxJava can cause deadlocks and application jank. " + "Prefer making the enclosing method/function return this $shortType, a Disposable to grant control to the caller,$orMessage or refactoring this in a way to use non-blocking calls. " + "If running in a test, use the .test()/TestObserver API (https://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/observers/TestObserver.html) test synchronous values.", issue = DenyListedApiDetector.BLOCKING_ISSUE, ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/eithernet/DoNotExposeEitherNetInRepositoriesDetector.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.eithernet import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Location import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.isJava import com.intellij.psi.PsiModifierListOwner import com.intellij.psi.PsiType import com.intellij.psi.util.PsiTypesUtil import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtModifierListOwner import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierTypeOrDefault import org.jetbrains.uast.UElement import org.jetbrains.uast.UField import org.jetbrains.uast.UMethod import org.jetbrains.uast.getContainingUClass import org.jetbrains.uast.java.isJava import slack.lint.util.safeReturnType import slack.lint.util.sourceImplementation private const val EITHERNET_PACKAGE = "com.slack.eithernet" /** Reports an error when returning EitherNet types directly in public repository APIs. */ class DoNotExposeEitherNetInRepositoriesDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> = listOf(UMethod::class.java, UField::class.java) override fun createUastHandler(context: JavaContext) = object : UElementHandler() { val isJava = isJava(context.uastFile?.lang) override fun visitMethod(node: UMethod) { if (node.sourcePsi is KtProperty) return // Handled by visitField check( node.isPublic, { node.isRepositoryMember }, { node.safeReturnType(context).isEitherNetType }, { context.getLocation(node.returnTypeReference ?: node) }, ) } override fun visitField(node: UField) { check( node.isPublic, { node.isRepositoryMember }, { node.type.isEitherNetType }, { context.getLocation(node.typeReference ?: node) }, ) } private val UElement.isPublic: Boolean get() { if (isJava && this is PsiModifierListOwner) { if (modifierList?.hasExplicitModifier("public") == true) return true } if (sourcePsi is KtModifierListOwner) { val ktModifierListOwner = sourcePsi as KtModifierListOwner val visibility = ktModifierListOwner.visibilityModifierTypeOrDefault() if (visibility == KtTokens.PUBLIC_KEYWORD) return true } return getContainingUClass()?.isInterface == true } private fun check( isPublic: Boolean, isRepositoryMember: () -> Boolean, isEitherNetType: () -> Boolean, location: () -> Location, ) { if (!isPublic) return if (!isRepositoryMember()) return if (isEitherNetType()) { context.report( issue = ISSUE, location = location(), message = "Repository APIs should not expose EitherNet types directly.", ) } } private val UElement.isRepositoryMember: Boolean get() { val containingClass = getContainingUClass() ?: return false return containingClass.name?.endsWith("Repository") == true } private val PsiType?.isEitherNetType: Boolean get() { return PsiTypesUtil.getPsiClass(this)?.qualifiedName?.startsWith(EITHERNET_PACKAGE) == true } } companion object { private fun Implementation.toIssue(): Issue { return Issue.create( id = "DoNotExposeEitherNetInRepositories", briefDescription = "Repository APIs should not expose EitherNet types directly.", explanation = "EitherNet (and networking in general) should be an implementation detail of the repository layer.", category = Category.CORRECTNESS, priority = 0, severity = Severity.ERROR, implementation = this, ) } val ISSUE: Issue = sourceImplementation().toIssue() } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/inclusive/InclusiveNamingChecker.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 @file:Suppress("UnstableApiUsage") package slack.lint.inclusive import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Location import com.android.tools.lint.detector.api.Position import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.StringOption import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.XmlContext import java.util.Locale import org.jetbrains.uast.UElement import org.w3c.dom.Node import slack.lint.util.Priorities import slack.lint.util.resourcesImplementation import slack.lint.util.sourceImplementation sealed class InclusiveNamingChecker { companion object { internal val BLOCK_LIST = StringOption( "block-list", "A comma-separated list of words that should not be used in source code.", null, "This property should define a comma-separated list of words that should not be used in source code.", ) private val SOURCE_ISSUE = sourceImplementation().toIssue() private val RESOURCES_ISSUE = resourcesImplementation().toIssue() val ISSUES: List = listOf(SOURCE_ISSUE, RESOURCES_ISSUE) private fun Implementation.toIssue(): Issue { return Issue.create( "InclusiveNaming", "Use inclusive naming.", """ We try to use inclusive naming at Slack. Terms such as blacklist, whitelist, master, slave, etc, while maybe \ widely used today, can be socially charged and make others feel excluded or uncomfortable. """ .trimIndent(), Category.CORRECTNESS, Priorities.NORMAL, Severity.ERROR, this, ) .setOptions(listOf(BLOCK_LIST)) } /** Loads a comma-separated list of blocked words from the [BLOCK_LIST] option. */ fun loadBlocklist(context: Context): Set { return BLOCK_LIST.getValue(context.configuration) ?.splitToSequence(",") .orEmpty() .map(String::trim) .filter(String::isNotBlank) .toSet() } } abstract val context: C abstract val blocklist: Set protected abstract val issue: Issue abstract fun locationFor(node: N): Location open fun shouldReport(node: N, location: Location, name: String, isFile: Boolean): Boolean = true fun check(node: N, name: String?, type: String, isFile: Boolean = false) { if (name == null) return val lowerCased = name.lowercase(Locale.US) blocklist .find { it in lowerCased } ?.let { matched -> val location = locationFor(node) if (!shouldReport(node, location, name, isFile)) return val description = buildString { append(issue.getBriefDescription(TextFormat.TEXT)) append(" Matched string is '") append(matched) append("' in ") append(type) append(" name '") append(name) append("'") } context.report(issue, location, description, null) } } class SourceCodeChecker(override val context: JavaContext, override val blocklist: Set) : InclusiveNamingChecker() { /** * Some element types will be reported multiple times (such as property parameters). This caches * reports so we only report once. */ private val cachedReports = mutableSetOf() override val issue: Issue = SOURCE_ISSUE override fun locationFor(node: UElement): Location { return context.getLocation(node) } override fun shouldReport( node: UElement, location: Location, name: String, isFile: Boolean, ): Boolean { return cachedReports.add(CacheKey.fromLocation(location, isFile)) } private data class CacheKey(val location: String) { companion object { fun fromLocation(location: Location, isFile: Boolean): CacheKey { val fileName = location.file.name val start: String val end: String if (isFile) { start = "" end = "" } else { start = location.start?.lineString ?: "" end = location.end?.lineString ?: "" } return CacheKey("$fileName-$start-$end") } private val Position.lineString: String get() { return "$line:$column" } } } } class XmlChecker(override val context: XmlContext, override val blocklist: Set) : InclusiveNamingChecker() { override val issue: Issue = RESOURCES_ISSUE override fun locationFor(node: Node): Location { return context.getLocation(node) } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/inclusive/InclusiveNamingResourceScanner.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.inclusive import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.ResourceXmlDetector import com.android.tools.lint.detector.api.XmlContext import org.w3c.dom.Attr import org.w3c.dom.Document import org.w3c.dom.Element @Suppress("UnstableApiUsage") class InclusiveNamingResourceScanner : ResourceXmlDetector() { private lateinit var blocklist: Set override fun beforeCheckRootProject(context: Context) { super.beforeCheckRootProject(context) blocklist = InclusiveNamingChecker.loadBlocklist(context) } override fun getApplicableElements(): List = ALL override fun visitAttribute(context: XmlContext, attribute: Attr) { if (blocklist.isEmpty()) return InclusiveNamingChecker.XmlChecker(context, blocklist) .check(attribute, attribute.name, "attribute") } override fun visitDocument(context: XmlContext, document: Document) { super.visitDocument(context, document) if (blocklist.isEmpty()) return } override fun visitElement(context: XmlContext, element: Element) { if (blocklist.isEmpty()) return } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/inclusive/InclusiveNamingSourceCodeScanner.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.inclusive import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.isKotlin import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.uast.UClass import org.jetbrains.uast.UField import org.jetbrains.uast.UFile import org.jetbrains.uast.ULabeledExpression import org.jetbrains.uast.ULocalVariable import org.jetbrains.uast.UMethod import org.jetbrains.uast.UParameter import org.jetbrains.uast.UVariable @Suppress("UnstableApiUsage") class InclusiveNamingSourceCodeScanner : Detector(), SourceCodeScanner { private lateinit var blocklist: Set override fun beforeCheckRootProject(context: Context) { super.beforeCheckRootProject(context) blocklist = InclusiveNamingChecker.loadBlocklist(context) } override fun getApplicableUastTypes() = listOf( UFile::class.java, UClass::class.java, UMethod::class.java, UVariable::class.java, ULabeledExpression::class.java, ) override fun createUastHandler(context: JavaContext): UElementHandler? { if (blocklist.isEmpty()) return null val checker = InclusiveNamingChecker.SourceCodeChecker(context, blocklist) context.uastFile?.let { uastFile -> checker.check(uastFile, context.file.name, "file", isFile = true) } return object : UElementHandler() { override fun visitFile(node: UFile) { checker.check(node, node.packageName, "package") } override fun visitClass(node: UClass) { checker.check(node, node.name, "class") } override fun visitMethod(node: UMethod) { if (node.isConstructor) return val type = if (isKotlin(node.language)) "function" else "method" checker.check(node, node.name, type) } // Covers parameters, properties, fields, and local vars override fun visitVariable(node: UVariable) { val type = when (node) { is UField -> { if (isKotlin(node.language)) { "property" } else { "field" } } is ULocalVariable -> "local variable" is UParameter -> { if (node.sourcePsi is KtProperty) { "property" } else { "parameter" } } else -> return } checker.check(node, node.name, type) } // Covers things like forEach label@ {} override fun visitLabeledExpression(node: ULabeledExpression) { checker.check(node, node.label, "label") } } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/AnyMockDetector.kt ================================================ // Copyright (C) 2024 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for any mocking. */ object AnyMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockAnything", "Do not add new mocks.", """ Mocking is almost always unnecessary and will make your tests more brittle. Use real instances \ (if appropriate) or test fakes instead. This lint is a catch-all for mocking, and has been \ enabled in this project to help prevent new mocking from being added. """, Category.CORRECTNESS, 6, // Off by default though Severity.ERROR, sourceImplementation(), ) .setEnabledByDefault(false) override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason = MockDetector.Reason(mockedType, "Do not add new mocks.") } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/AutoValueMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.Severity import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking AutoValue classes. */ object AutoValueMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockAutoValue", "AutoValue classes represent pure data classes, so mocking them should not be necessary.", """ `AutoValue` classes represent pure data classes, so mocking them should not be necessary. \ Construct a real instance of the class instead. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override val annotations: Set = setOf("com.google.auto.value.AutoValue", "com.google.auto.value.AutoValue.Builder") } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/DataClassMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking Kotlin data classes. */ object DataClassMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockDataClass", "data classes represent pure data classes, so mocking them should not be necessary.", """ data classes represent pure data classes, so mocking them should not be necessary. \ Construct a real instance of the class instead. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override val annotations: Set = emptySet() override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { // Don't warn on records because we have a separate check for that return if (evaluator.isData(mockedType) && !mockedType.hasAnnotation("kotlin.jvm.JvmRecord")) { MockDetector.Reason( mockedType, "'${mockedType.qualifiedName}' is a data class, so mocking it should not be necessary", ) } else { null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/DoNotMockMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import org.jetbrains.uast.UClass import org.jetbrains.uast.toUElementOfType import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking `@DoNotMock`-annotated classes. */ object DoNotMockMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMock", "", // We always compute the brief description. """ Do not mock classes annotated with `@DoNotMock`, as they are explicitly asking not to be \ mocked in favor of better options (test fakes, etc). These types should define \ explanations/alternatives in their annotation. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private const val FQCN_SLACK_DNM = "slack.lint.annotations.DoNotMock" private const val FQCN_EP_DNM = "com.google.errorprone.annotations.DoNotMock" override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { val uMockedType = mockedType.toUElementOfType() ?: return null val doNotMockAnnotation = uMockedType.findAnnotation(FQCN_SLACK_DNM) ?: uMockedType.findAnnotation(FQCN_EP_DNM) ?: return null val messagePrefix = "Do not mock ${mockedType.name}" val suffix = doNotMockAnnotation.findAttributeValue("value")?.evaluate() as String? val message = if (suffix == null) { messagePrefix } else { "$messagePrefix: $suffix" } return MockDetector.Reason(mockedType, message) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/ErrorProneDoNotMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat.TEXT import org.jetbrains.uast.UClass import slack.lint.util.sourceImplementation /** * A simple checker that checks for use of error-prone's `@DoNotMock` and suggests replacing with * our slack.lint.annotation version. */ class ErrorProneDoNotMockDetector : Detector(), SourceCodeScanner { companion object { val ISSUE: Issue = Issue.create( "ErrorProneDoNotMockUsage", "Use Slack's internal `@DoNotMock` annotation.", """ While error-prone has a `@DoNotMock` annotation, prefer to use Slack's internal one as it's \ not specific to error-prone and won't go away in a Java-less world. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) private const val FQCN_SLACK_DNM = "slack.lint.annotations.DoNotMock" private const val FQCN_EP_DNM = "com.google.errorprone.annotations.DoNotMock" } override fun getApplicableUastTypes() = listOf(UClass::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitClass(node: UClass) { node.findAnnotation(FQCN_EP_DNM)?.let { epDoNotMock -> val replacement = if ("@$FQCN_EP_DNM" in epDoNotMock.sourcePsi!!.text) { "@$FQCN_EP_DNM" } else { "@DoNotMock" } context.report( ISSUE, context.getLocation(epDoNotMock), ISSUE.getBriefDescription(TEXT), quickfixData = fix() .replace() .name("Replace with slack.lint.annotations.DoNotMock") .range(context.getLocation(epDoNotMock)) .shortenNames() .text(replacement) .with("@$FQCN_SLACK_DNM") .autoFix() .build(), ) } } } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/MockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.StringOption import com.android.tools.lint.detector.api.isJava import com.android.tools.lint.detector.api.isKotlin import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import java.util.Locale import kotlin.io.path.bufferedWriter import kotlin.io.path.createDirectories import kotlin.io.path.createFile import kotlin.io.path.deleteExisting import kotlin.io.path.exists import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UClassLiteralExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UField import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.UastCallKind import slack.lint.mocking.MockDetector.Companion.TYPE_CHECKERS import slack.lint.mocking.MockDetector.TypeChecker import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.OptionLoadingDetector import slack.lint.util.StringSetLintOption private data class MockFactory(val declarationContainer: String, val factoryName: String) /** * A detector for detecting different kinds of mocking behavior. Implementations of [TypeChecker] * can indicate annotated types that should be reported via [TypeChecker.checkType] or * [TypeChecker.annotations] for more dynamic control. * * New [TypeChecker] implementations should be added to [TYPE_CHECKERS] to run in this. */ class MockDetector @JvmOverloads constructor( private val mockAnnotationsOption: StringSetLintOption = StringSetLintOption(MOCK_ANNOTATIONS), private val mockFactoriesOption: StringSetLintOption = StringSetLintOption(MOCK_FACTORIES), private val mockReport: StringOption = MOCK_REPORT, ) : OptionLoadingDetector(mockAnnotationsOption, mockFactoriesOption), SourceCodeScanner { companion object { internal const val MOCK_REPORT_PATH = "build/reports/mockdetector/mock-report.csv" internal val MOCK_ANNOTATIONS = StringOption( "mock-annotations", "A comma-separated list of mock annotations.", "org.mockito.Mock,org.mockito.Spy", "This property should define comma-separated list of mock annotation class names (FQCN). Set this for all issues using this option.", ) internal val MOCK_FACTORIES = StringOption( "mock-factories", "A comma-separated list of mock factories (org.mockito.Mockito#methodName).", "org.mockito.Mockito#mock,org.mockito.Mockito#spy,slack.test.mockito.MockitoHelpers#mock,slack.test.mockito.MockitoHelpersKt#mock,org.mockito.kotlin.MockingKt#mock,org.mockito.kotlin.SpyingKt#spy", "A comma-separated list of mock factories (org.mockito.Mockito#methodName). Set this for all issues using this option.", ) internal val MOCK_REPORT = StringOption( "mock-report", "If enabled, writes a mock report to /$MOCK_REPORT_PATH.", "none", "If enabled, writes a mock report to /$MOCK_REPORT_PATH. The format of the file is a csv of (type,isError) of mocked classes. Set this for all issues using this option.", ) private val TYPE_CHECKERS = listOf( // Loosely defined in the order of most likely to be hit AnyMockDetector, PlatformTypeMockDetector, DataClassMockDetector, DoNotMockMockDetector, SealedClassMockDetector, AutoValueMockDetector, ObjectClassMockDetector, RecordClassMockDetector, ) private val OPTIONS = listOf(MOCK_ANNOTATIONS, MOCK_FACTORIES, MOCK_REPORT) val ALL_ISSUES = TYPE_CHECKERS.map { it.issue.setOptions(OPTIONS) } val ISSUES = ALL_ISSUES.filterNot { it == AnyMockDetector.issue }.toTypedArray() } // A mapping of mocked types private val reports = mutableListOf>() override fun getApplicableUastTypes() = listOf(UCallExpression::class.java, UField::class.java) override fun createUastHandler(context: JavaContext): UElementHandler? { if (!context.isTestSource) return null val checkers = TYPE_CHECKERS.filter { context.isEnabled(it.issue) } .ifEmpty { return null } val slackEvaluator = MetadataJavaEvaluator(context.file.name, context.evaluator) val reportingMode = reportMode(context) val reportErrors = reportingMode == MockReportMode.ALL || reportingMode == MockReportMode.ERRORS val reportAll = reportingMode == MockReportMode.ALL val mockFactories: Map> = mockFactoriesOption.value .map { factory -> val (declarationContainer, factoryName) = factory.split("#") MockFactory(declarationContainer, factoryName) } .groupBy { it.factoryName } .mapValues { it.value.mapTo(mutableSetOf()) { it.declarationContainer } } return object : UElementHandler() { // Checks for mock()/spy() calls override fun visitCallExpression(node: UCallExpression) { // We only want method calls if (node.kind != UastCallKind.METHOD_CALL) return // Check our known mock methods val mockFactoryContainers = mockFactories[node.methodName]?.takeIf { it.isNotEmpty() } ?: return // Matches a known method, now check if this one's in that method's known declaration // container val resolvedContainer = node.resolve()?.let { it.containingClass?.qualifiedName ?: context.evaluator.getPackage(it)?.qualifiedName } ?: return if (resolvedContainer !in mockFactoryContainers) return // Now resolve the mocked type var argumentType: PsiClass? = null val expressionType = node.getExpressionType() when { expressionType != null -> { argumentType = slackEvaluator.getTypeClass(expressionType) } node.typeArgumentCount == 1 -> { // We can read the type here for the fun mock() helpers argumentType = slackEvaluator.getTypeClass(node.typeArguments[0]) } node.valueArgumentCount != 0 -> { when (val firstArg = node.valueArguments[0]) { is UClassLiteralExpression -> { // It's Foo.class, we can just use it directly argumentType = slackEvaluator.getTypeClass(firstArg.type) } is UReferenceExpression -> { val type = firstArg.getExpressionType() if (node.methodName == "spy") { // spy takes an instance, so take the type at face value argumentType = slackEvaluator.getTypeClass(type) } else if (type is PsiClassType && type.parameterCount == 1) { // If it's a Class and not a "spy" method, assume it's the mock type val classGeneric = type.parameters[0] argumentType = slackEvaluator.getTypeClass(classGeneric) } } } } } argumentType?.let { checkMock(node, argumentType) } } // Checks properties and fields, usually annotated with @Mock/@Spy override fun visitField(node: UField) { if (isKotlin(node.language)) { val sourcePsi = node.sourcePsi ?: return if (sourcePsi is KtProperty && isMockAnnotated(node)) { val type = slackEvaluator.getTypeClass(node.type) ?: return checkMock(node, type) return } } else if (isJava(node.language) && isMockAnnotated(node)) { val type = slackEvaluator.getTypeClass(node.type) ?: return checkMock(node, type) return } } private fun isMockAnnotated(node: UAnnotated): Boolean { return mockAnnotationsOption.value.any { node.findAnnotation(it) != null } } private fun addReport(type: PsiClass, isError: Boolean) { if (reportAll || (isError && reportErrors)) { type.qualifiedName?.let { reports += (it to isError) } } } private fun checkMock(node: UElement, type: PsiClass) { for (checker in checkers) { val reason = checker.checkType(context, slackEvaluator, type) if (reason != null) { addReport(type, isError = true) context.report(checker.issue, context.getLocation(node), reason.reason) return } val disallowedAnnotation = checker.annotations.find { type.hasAnnotation(it) } ?: continue addReport(type, isError = true) context.report( checker.issue, context.getLocation(node), "Mocked type is annotated with non-mockable annotation $disallowedAnnotation", ) return } addReport(type, isError = false) } } } private fun reportMode(context: Context): MockReportMode { return mockReport.getValue(context)?.let { MockReportMode.valueOf(it.uppercase(Locale.US)) } ?: MockReportMode.NONE } override fun afterCheckEachProject(context: Context) { if (reportMode(context) != MockReportMode.NONE && reports.isNotEmpty()) { val outputFile = context.project.dir.toPath().resolve(MOCK_REPORT_PATH) if (outputFile.exists()) { outputFile.deleteExisting() } // TODO use createParentDirectories() when we upgrade to Kotlin 1.9 outputFile.parent.createDirectories() outputFile.createFile() outputFile.bufferedWriter().use { writer -> writer.write( reports .sortedBy { it.first } .joinToString(prefix = "type,isError\n", separator = "\n") { (type, isError) -> "$type,$isError" } ) } } reports.clear() } interface TypeChecker { val issue: Issue /** Set of annotation FQCNs that should not be mocked */ val annotations: Set get() = emptySet() fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): Reason? { return null } } /** * @property type a [PsiClass] object representing the class that should not be mocked. * @property reason The reason this class should not be mocked, which may be as simple as "it is * annotated to forbid mocking" but may also provide a suggested workaround. */ data class Reason(val type: PsiClass, val reason: String) /** Represents different modes for generating mock reports. */ enum class MockReportMode { /** The default – no reporting is done. */ NONE, /** Only DoNotMock errors are reported. */ ERRORS, /** All mocked types are reported, even types that are not errors. */ ALL, } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/ObjectClassMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking Kotlin object classes. */ object ObjectClassMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockObjectClass", "object classes are singletons, so mocking them should not be necessary", """ object classes are global singletons, so mocking them should not be necessary. \ Use the object instance instead. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override val annotations: Set = emptySet() override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { return if (evaluator.isObject(mockedType)) { MockDetector.Reason( mockedType, "'${mockedType.qualifiedName}' is an object, so mocking it should not be necessary", ) } else { null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/PlatformTypeMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking platform types. */ object PlatformTypeMockDetector : MockDetector.TypeChecker { private val platforms = setOf("android", "androidx", "java", "javax", "kotlin", "kotlinx", "scala") override val issue: Issue = Issue.create( "DoNotMockPlatformTypes", "platform types should not be mocked", """ Platform types (i.e. classes in java.*, android.*, etc) should not be mocked. \ Use a real instance or fake instead. Mocking platform types like these can lead \ to surprising test failures due to mocks that actually behave differently than \ real instances, especially when passed into real implementations that use them \ and expect them to behave in a certain way. If using Robolectric, consider using \ its shadow APIs. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { val name = mockedType.qualifiedName ?: return null val isPlatformType = name.substringBefore('.') in platforms return if (isPlatformType) { MockDetector.Reason(mockedType, "platform type '$name' should not be mocked") } else { null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/RecordClassMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.isJava import com.intellij.psi.PsiClass import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking record classes. */ object RecordClassMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockRecordClass", "record classes represent pure data classes, so mocking them should not be necessary.", """ record classes represent pure data classes, so mocking them should not be necessary. \ Construct a real instance of the class instead. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override val annotations: Set = emptySet() override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { val isRecord = if (isJava(mockedType.language)) { // Java mockedType.isRecord } else { // Kotlin. Check the annotation first as the isData check may check metadata mockedType.hasAnnotation("kotlin.jvm.JvmRecord") && evaluator.isData(mockedType) } return if (isRecord) { MockDetector.Reason( mockedType, "'${mockedType.qualifiedName}' is a record class, so mocking it should not be necessary", ) } else { null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/SealedClassMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import com.intellij.psi.impl.light.LightElement import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking Kotlin sealed classes. */ object SealedClassMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockSealedClass", "sealed classes have a restricted type hierarchy, use a subtype instead", """ sealed classes have a restricted type hierarchy, so creating new unrestricted mocks \ will break runtime expectations. Mockito also cannot mock sealed classes in Java 17+. \ Use a subtype instead. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override val annotations: Set = emptySet() override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { // Check permitsList to cover Java 17 sealed types too return if (evaluator.isSealed(mockedType) || mockedType.hasPermitsList()) { MockDetector.Reason( mockedType, "'${mockedType.qualifiedName}' is a sealed type and has a restricted type hierarchy, use a subtype instead.", ) } else { null } } private fun PsiClass.hasPermitsList(): Boolean { return if (this is LightElement) { // TODO https://issuetracker.google.com/issues/428697839 false } else { permitsList != null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/mocking/ValueClassMockDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiClass import slack.lint.util.MetadataJavaEvaluator import slack.lint.util.isValueClass import slack.lint.util.sourceImplementation /** A [MockDetector.TypeChecker] that checks for mocking Kotlin value classes. */ // TODO not currently enabled because of https://issuetracker.google.com/issues/283715187 object ValueClassMockDetector : MockDetector.TypeChecker { override val issue: Issue = Issue.create( "DoNotMockValueClass", "value classes represent inlined types, so mocking them should not be necessary.", """ value classes represent inlined types, so mocking them should not be necessary. \ Construct a real instance of the class instead. """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) override val annotations: Set = emptySet() override fun checkType( context: JavaContext, evaluator: MetadataJavaEvaluator, mockedType: PsiClass, ): MockDetector.Reason? { return if (evaluator.isValueClass(mockedType)) { MockDetector.Reason( mockedType, "'${mockedType.qualifiedName}' is a value class using inlined types, so mocking it should not be necessary", ) } else { null } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/moshi/MoshiLintUtil.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.moshi import com.intellij.psi.PsiClass internal object MoshiLintUtil { private const val FQCN_ANNOTATION_ADAPTED_BY = "dev.zacsweers.moshix.adapters.AdaptedBy" private const val FQCN_ANNOTATION_JSON_CLASS = "com.squareup.moshi.JsonClass" fun PsiClass.hasMoshiAnnotation(): Boolean { return hasAnnotation(FQCN_ANNOTATION_ADAPTED_BY) || hasAnnotation(FQCN_ANNOTATION_JSON_CLASS) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/parcel/ParcelizeFunctionPropertyDetector.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.parcel import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.getUMethod import com.intellij.psi.PsiClassType import com.intellij.psi.PsiType import org.jetbrains.kotlin.psi.KtPrimaryConstructor import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.kotlin.isKotlin import slack.lint.parcel.ParcelizeFunctionPropertyDetector.Companion.ISSUE import slack.lint.util.sourceImplementation /** @see ISSUE */ class ParcelizeFunctionPropertyDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List> { return listOf(UClass::class.java) } override fun createUastHandler(context: JavaContext): UElementHandler? { // Parcelize can only be used in Kotlin files, so this check only checks in Kotlin files if (!isKotlin(context.uastFile?.lang)) return null return object : UElementHandler() { override fun visitClass(node: UClass) { if (!node.hasAnnotation(PARCELIZE)) return // Primary constructor is required, but Parcelize's inspections will catch this val primaryConstructor = node.constructors .asSequence() .mapNotNull { it.getUMethod() } .firstOrNull { it.sourcePsi is KtPrimaryConstructor } ?: return // Now check properties for (parameter in primaryConstructor.uastParameters) { if (parameter.type.isFunctionType && !parameter.hasAnnotation(IGNORED_ON_PARCEL)) { context.report( ISSUE, context.getLocation(parameter.typeReference), ISSUE.getExplanation(TextFormat.TEXT), ) } } } private val PsiType.isFunctionType: Boolean get() = this is PsiClassType && resolve()?.qualifiedName?.startsWith("kotlin.jvm.functions.Function") == true } } companion object { private const val PARCELIZE_PACKAGE = "kotlinx.parcelize" private const val PARCELIZE = "$PARCELIZE_PACKAGE.Parcelize" private const val IGNORED_ON_PARCEL = "$PARCELIZE_PACKAGE.IgnoredOnParcel" internal val ISSUE: Issue = Issue.create( "ParcelizeFunctionProperty", "Function type properties are not parcelable", "While technically (and surprisingly) supported by Parcelize, function types " + "should not be used in Parcelize classes. There are only limited conditions where it " + "will work and it's usually a sign that you're modeling your data wrong.", Category.CORRECTNESS, 9, Severity.ERROR, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/resources/FullyQualifiedResourceDetector.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.isKotlin import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.uast.UElement import org.jetbrains.uast.UQualifiedReferenceExpression import slack.lint.resources.ImportAliasesLoader.IMPORT_ALIASES import slack.lint.util.sourceImplementation private const val FQN_ANDROID_R = "android.R" private val WHITESPACE_REGEX = "\\s+".toRegex() /** Reports an error when an R class is referenced using its fully qualified name. */ class FullyQualifiedResourceDetector : Detector(), SourceCodeScanner { private lateinit var importAliases: Map override fun beforeCheckRootProject(context: Context) { super.beforeCheckRootProject(context) importAliases = ImportAliasesLoader.loadImportAliases(context) } override fun getApplicableUastTypes(): List> = listOf(UQualifiedReferenceExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) { // Import alias is a Kotlin feature. if (!isKotlin(node.lang)) return val qualifier = node.receiver.asSourceString() val normalized = qualifier.trim().replace(WHITESPACE_REGEX, "") if (normalized.endsWith(".R") && normalized != FQN_ANDROID_R) { val alias = importAliases[normalized] context.report( ISSUE, context.getNameLocation(node.receiver), if (alias != null) "Use $alias as an import alias instead" else "Use an import alias instead", quickfixData = createLintFix(alias, node, qualifier), ) } } private fun createLintFix( alias: String?, node: UQualifiedReferenceExpression, qualifier: String, ): LintFix? { return if (alias != null) { val fixes = mutableListOf( fix().replace().range(context.getLocation(node.receiver)).with(alias).build() ) // Alternative to ReplaceStringBuilder#imports since that one didn't work here. addImportIfMissing(qualifier, alias, fixes) fix() .name("Replace with import alias") .composite(*fixes.reversed().toTypedArray()) .autoFix() } else { null } } private fun addImportIfMissing( qualifier: String, alias: String, issues: MutableList, ) { context.uastFile?.imports?.let { imports -> val importIsMissing = !imports.any { val importDirective = it.sourcePsi as? KtImportDirective val importedFqNameString = importDirective?.importedFqName?.asString()?.trim() qualifier == importedFqNameString && alias == importDirective.aliasName } if (importIsMissing) { if (imports.isNotEmpty()) { val lastImport = imports.last().sourcePsi as? KtImportDirective addImportAfterLastImport(lastImport, qualifier, alias, issues) } else { addImportAfterPackageName(qualifier, alias, issues) } } } } private fun addImportAfterLastImport( lastImport: KtImportDirective?, qualifier: String, alias: String, issues: MutableList, ) { if (lastImport != null) { issues.add( fix() .replace() .range(context.getLocation(lastImport)) .with(lastImport.text + System.lineSeparator() + "import $qualifier as $alias") .build() ) } } private fun addImportAfterPackageName( qualifier: String, alias: String, issues: MutableList, ) { (context.psiFile as? KtFile)?.packageDirective?.let { packageDirective -> issues.add( fix() .replace() .range(context.getLocation(packageDirective)) .with( packageDirective.text + System.lineSeparator() + System.lineSeparator() + "import $qualifier as $alias" ) .build() ) } } } } companion object { val ISSUE: Issue = Issue.create( "FullyQualifiedResource", "Resources should use an import alias instead of being fully qualified.", "Resources should use an import alias instead of being fully qualified. For example: \n" + "import slack.l10n.R as L10nR\n" + "...\n" + "...getString(L10nR.string.app_name)", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) .setOptions(listOf(IMPORT_ALIASES)) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/resources/ImportAliasesLoader.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.StringOption object ImportAliasesLoader { internal val IMPORT_ALIASES = StringOption( "import-aliases", "A comma-separated list of package name and their import aliases.", null, "This property should define a comma-separated list of package name and their import aliases" + " in the format: packageName as importAlias", ) /** Loads the import aliases from the [IMPORT_ALIASES] option. */ fun loadImportAliases(context: Context): Map { return IMPORT_ALIASES.getValue(context.configuration) ?.splitToSequence(",") .orEmpty() .map(String::trim) .filter(String::isNotBlank) .map { val (packageName, alias) = it.split(" as ") packageName.trim() to alias.trim() } .toMap() } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/resources/MissingResourceImportAliasDetector.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.uast.UElement import org.jetbrains.uast.UImportStatement import org.jetbrains.uast.USimpleNameReferenceExpression import org.jetbrains.uast.getContainingUFile import org.jetbrains.uast.getQualifiedParentOrThis import slack.lint.resources.ImportAliasesLoader.IMPORT_ALIASES import slack.lint.resources.model.RootIssueData import slack.lint.util.sourceImplementation /** * Reports an error when an R class, other than the local module's, is imported without an import * alias. */ class MissingResourceImportAliasDetector : Detector(), SourceCodeScanner { private val fixes = mutableListOf() private var rootIssueData: RootIssueData? = null private lateinit var importAliases: Map override fun beforeCheckRootProject(context: Context) { super.beforeCheckRootProject(context) importAliases = ImportAliasesLoader.loadImportAliases(context) } override fun afterCheckFile(context: Context) { // Collect all the fixes and apply them to one issue on the import to avoid adding the import // alias with a fix // and leaving the R references still referencing the non-aliased R or vice versa. rootIssueData?.let { context.report( ISSUE, it.nameLocation, "Use an import alias for R classes from other modules", quickfixData = fix() .name("Add import alias") // Apply the fixes in reverse so that the ranges/locations don't change. .composite(*fixes.reversed().toTypedArray()) .autoFix(), ) rootIssueData = null fixes.clear() } } override fun getApplicableUastTypes(): List> = listOf(UImportStatement::class.java, USimpleNameReferenceExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitImportStatement(node: UImportStatement) { // Import alias is a Kotlin feature. val importDirective = node.sourcePsi as? KtImportDirective ?: return if (importDirective.importedName?.identifier == "R" && importDirective.aliasName == null) { val packageName = context.project.`package` val filePackageName = node.getContainingUFile()?.packageName val importedFqName = importDirective.importedFqName val parentImportedFqName = importedFqName?.parent()?.asString() val isLocalResourceImport = parentImportedFqName?.let { if (packageName != null) it == packageName else filePackageName?.startsWith(it) } ?: true if (!isLocalResourceImport) { val importedFqNameString = requireNotNull(importedFqName).asString() val alias = importAliases[importedFqNameString] if (alias != null) { rootIssueData = RootIssueData( alias = alias, nameLocation = context.getNameLocation(importDirective), ) fixes.add(createImportLintFix(importDirective, importedFqNameString, alias)) } else { context.report( ISSUE, context.getNameLocation(importDirective), "Use an import alias for R classes from other modules", ) } } } } private fun createImportLintFix( importDirective: KtImportDirective, importedFqNameString: String, alias: String?, ): LintFix { return fix() .replace() .range(context.getLocation(importDirective)) .text(importedFqNameString) .with("$importedFqNameString as $alias") .build() } override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) { rootIssueData?.alias?.let { if ( node.asSourceString() == "R" && // Make sure node is its own parent to safeguard against cases like // Build.VERSION_CODES.R. node.getQualifiedParentOrThis() == node ) { fixes.add(createReferenceLintFix(node, it)) } } } private fun createReferenceLintFix( node: USimpleNameReferenceExpression, alias: String, ): LintFix { return fix().replace().range(context.getLocation(node)).with(alias).build() } } } companion object { val ISSUE: Issue = Issue.create( "MissingResourceImportAlias", "Missing import alias for R class.", """ Only the local module's R class is allowed to be imported without an alias. \ Add an import alias for this. For example, import slack.l10n.R as L10nR """, Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) .setOptions(listOf(IMPORT_ALIASES)) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/resources/WrongResourceImportAliasDetector.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.uast.UElement import org.jetbrains.uast.UImportStatement import org.jetbrains.uast.USimpleNameReferenceExpression import slack.lint.resources.ImportAliasesLoader.IMPORT_ALIASES import slack.lint.resources.model.RootIssueData import slack.lint.util.sourceImplementation /** Reports an error when an R class is imported using the wrong import alias. */ class WrongResourceImportAliasDetector : Detector(), SourceCodeScanner { private val fixes = mutableListOf() private var rootIssueData: RootIssueData? = null private lateinit var importAliases: Map override fun beforeCheckRootProject(context: Context) { super.beforeCheckRootProject(context) importAliases = ImportAliasesLoader.loadImportAliases(context) } override fun afterCheckFile(context: Context) { // Collect all the fixes and apply them to one issue on the import to avoid renaming the import // alias with a fix // and leaving the R references still referencing the old import alias or vice versa. rootIssueData?.let { context.report( ISSUE, it.nameLocation, "Use ${it.alias} as an import alias here", quickfixData = fix() .name("Replace import alias") // Apply the fixes in reverse so that the ranges/locations don't change. .composite(*fixes.reversed().toTypedArray()) .autoFix(), ) reset() } } private fun reset() { rootIssueData = null fixes.clear() } override fun getApplicableUastTypes(): List> = listOf(UImportStatement::class.java, USimpleNameReferenceExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { private var wrongAlias: String? = null override fun visitImportStatement(node: UImportStatement) { // Import alias is a Kotlin feature. val importDirective = node.sourcePsi as? KtImportDirective ?: return // In case of multiple wrong aliases, only fix the first one. if (wrongAlias != null) return val importedFqNameString = importDirective.importedFqName?.asString() ?: return if (importedFqNameString.endsWith(".R") && importDirective.aliasName != null) { importAliases[importedFqNameString]?.let { alias -> val aliasName = importDirective.aliasName if (alias != aliasName) { this.wrongAlias = aliasName this@WrongResourceImportAliasDetector.rootIssueData = RootIssueData( alias = alias, nameLocation = context.getNameLocation(importDirective), ) fixes.add(createImportLintFix(node, importedFqNameString, aliasName, alias)) } } } } private fun createImportLintFix( node: UImportStatement, importedFqNameString: String, aliasName: String?, alias: String, ): LintFix { return fix() .replace() .range(context.getLocation(node)) .text("$importedFqNameString as $aliasName") .with("$importedFqNameString as $alias") .build() } override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) { wrongAlias?.let { if (node.asSourceString() == it) { fixes.add(createReferenceLintFix(node)) } } } private fun createReferenceLintFix(node: USimpleNameReferenceExpression): LintFix { return fix().replace().range(context.getLocation(node)).with(rootIssueData?.alias).build() } } } companion object { val ISSUE: Issue = Issue.create( "WrongResourceImportAlias", "Wrong import alias for this R class.", "R class import aliases should be consistent across the codebase. For example: \n" + "import slack.l10n.R as L10nR\n" + "import slack.uikit.R as UiKitR", Category.CORRECTNESS, 6, Severity.ERROR, sourceImplementation(), ) .setOptions(listOf(IMPORT_ALIASES)) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/resources/model/RootIssueData.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources.model import com.android.tools.lint.detector.api.Location data class RootIssueData(val alias: String, val nameLocation: Location) ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/retrofit/RetrofitUsageDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.retrofit import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector 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.Location import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiTypes import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.sourcePsiElement import slack.lint.util.removeNode import slack.lint.util.safeReturnType import slack.lint.util.sourceImplementation /** * A simple detector that validates basic Retrofit usage. * - Retrofit endpoints must be annotated with a retrofit method API unless they're an extension * function or private. * - `@FormUrlEncoded` must use `@POST`, `@PUT`, or `@PATCH`. * - `@Body` parameter requires `@POST`, `@PUT`, or `@PATCH`. * - `@Field` parameters require it to be annotated with `@FormUrlEncoded`. * - Must return something other than [Unit]. */ class RetrofitUsageDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UMethod::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitMethod(node: UMethod) { val httpAnnotation = HTTP_ANNOTATIONS.firstNotNullOfOrNull { node.findAnnotation(it) } ?: return val returnType = node.safeReturnType(context) val isVoidOrUnitReturnType = returnType == null || returnType == PsiTypes.voidType() || returnType.canonicalText == "kotlin.Unit" if (isVoidOrUnitReturnType) { val allowsUnitResult = node.hasAnnotation("slack.lint.annotations.AllowUnitResult") val isSuspend = context.evaluator.isSuspend(node) if (!(isSuspend && allowsUnitResult)) { node.report( "Retrofit endpoints should return something other than Unit/void.", context.getNameLocation(node), ) } } val httpAnnotationFqnc = httpAnnotation.qualifiedName ?: return val isBodyMethod = httpAnnotationFqnc in HTTP_BODY_ANNOTATIONS val annotationsByFqcn = node.uAnnotations.associateBy { it.qualifiedName } val isFormUrlEncoded = FQCN_FORM_ENCODED in annotationsByFqcn if (isFormUrlEncoded && !isBodyMethod) { node.report("@FormUrlEncoded requires @PUT, @POST, or @PATCH.") return } val isMultipart = FQCN_MULTIPART in annotationsByFqcn if (isMultipart && !isBodyMethod) { node.report("@Multipart requires @PUT, @POST, or @PATCH.") return } val hasPath = (httpAnnotation.findDeclaredAttributeValue("value")?.evaluate() as? String)?.isNotBlank() ?: false var hasBodyParam = false var hasFieldParams = false var hasPartParams = false var hasUrlParam = false for (parameter in node.uastParameters) { if (parameter.hasAnnotation(FQCN_BODY)) { if (!isBodyMethod) { httpAnnotation.report("@Body param requires @PUT, @POST, or @PATCH.") } else if (hasBodyParam) { parameter.report("Duplicate @Body param!.") } else { hasBodyParam = true } } else if ( parameter.hasAnnotation(FQCN_FIELD) || parameter.hasAnnotation(FQCN_FIELD_MAP) ) { hasFieldParams = true if (!isFormUrlEncoded) { val currentText = node.text node.report( "@Field(Map) param requires @FormUrlEncoded.", quickFixData = LintFix.create() .replace() .text(currentText) .with("@$FQCN_FORM_ENCODED\n$currentText") .autoFix() .build(), ) } } else if (parameter.hasAnnotation(FQCN_URL)) { if (hasPath) { httpAnnotation.report("@Url param should be used with an empty path.") } else { hasUrlParam = true } } else if (parameter.hasAnnotation(FQCN_PART)) { if (!isBodyMethod) { httpAnnotation.report("@Part param requires @PUT, @POST, or @PATCH.") } else { hasPartParams = true } } } if (isFormUrlEncoded) { if (!hasFieldParams) { val annotation = annotationsByFqcn.getValue(FQCN_FORM_ENCODED) annotation.report( "@FormUrlEncoded but has no @Field(Map) parameters.", quickFixData = LintFix.create().removeNode(context, annotation.sourcePsiElement!!), ) } } else if (isMultipart) { if (hasBodyParam || hasFieldParams) { httpAnnotation.report("@Multipart methods should only contain @Part parameters.") } else if (!hasPartParams) { httpAnnotation.report("@Multipart methods should contain at least one @Part parameter.") } } else if (isBodyMethod && !hasBodyParam && !hasFieldParams && !hasPartParams) { httpAnnotation.report("This annotation requires an `@Body` parameter.") } if (!hasPath && !hasUrlParam) { httpAnnotation.report("Http path is empty but has no @Url parameter.") } } private fun UElement.report( briefDescription: String, location: Location = context.getLocation(this), quickFixData: LintFix? = null, ) { context.report(ISSUE, location, briefDescription, quickfixData = quickFixData) } } } companion object { private val HTTP_ANNOTATIONS = setOf( "retrofit2.http.DELETE", "retrofit2.http.GET", "retrofit2.http.HEAD", "retrofit2.http.OPTIONS", "retrofit2.http.PATCH", "retrofit2.http.POST", "retrofit2.http.PUT", ) private val HTTP_BODY_ANNOTATIONS = setOf("retrofit2.http.PATCH", "retrofit2.http.POST", "retrofit2.http.PUT") private const val FQCN_FORM_ENCODED = "retrofit2.http.FormUrlEncoded" private const val FQCN_MULTIPART = "retrofit2.http.Multipart" private const val FQCN_FIELD = "retrofit2.http.Field" private const val FQCN_PART = "retrofit2.http.Part" private const val FQCN_FIELD_MAP = "retrofit2.http.FieldMap" private const val FQCN_BODY = "retrofit2.http.Body" private const val FQCN_URL = "retrofit2.http.Url" val ISSUE: Issue = Issue.create( "RetrofitUsage", "This is replaced by the caller.", "This linter reports various common configuration issues with Retrofit.", Category.CORRECTNESS, 10, Severity.ERROR, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/rx/RxObservableEmitDetector.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.rx import com.android.tools.lint.checks.DataFlowAnalyzer import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiClassType import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.ULambdaExpression import slack.lint.util.sourceImplementation class RxObservableEmitDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val issue = functionToIssue[node.methodName] ?: return val lambdaExpression = node.valueArguments.lastOrNull() as? ULambdaExpression ?: return val producerScopeParam = lambdaExpression.parameters.firstOrNull() ?: return val producerScopeType = (producerScopeParam.type as? PsiClassType) ?: return // Verify the parameter is a ProducerScope if (producerScopeType.resolve()?.qualifiedName != PROVIDER_SCOPE_FQN) return var sendCalled = false val visitor = object : DataFlowAnalyzer(emptySet()) { override fun visitCallExpression(node: UCallExpression): Boolean { // If we find a nested factory method, return immediately and stop traversing this // code path. // Note: this factory will be validated by the UElementHandler above if (node.methodName in functionToIssue) return true if (node.hasProviderScopeReceiver() && node.methodName in REQUIRE_ONE_OF) { sendCalled = true } return false } } lambdaExpression.accept(visitor) if (!sendCalled) { context.report( issue, context.getLocation(node), "${node.methodName} does not call `send()` or `trySend()`", ) } } } } private fun UCallExpression.hasProviderScopeReceiver(): Boolean = receiverType?.canonicalText?.startsWith(PROVIDER_SCOPE_FQN) == true internal companion object { private val REQUIRE_ONE_OF = setOf("send", "trySend") private const val PROVIDER_SCOPE_FQN = "kotlin.coroutines.ProducerScope" private val ISSUE_RX_OBSERVABLE_DOES_NOT_EMIT = Issue.create( id = "RxObservableDoesNotEmit", briefDescription = "RxObservable should call `send()` or `trySend()`", explanation = "If the rxObservable trailing lambda does not call `send()` or `trySend()`, the returned Observable will never emit!", category = Category.CORRECTNESS, priority = 2, severity = Severity.INFORMATIONAL, implementation = sourceImplementation(), ) private val ISSUE_RX_FLOWABLE_DOES_NOT_EMIT = Issue.create( id = "RxFlowableDoesNotEmit", briefDescription = "RxFlowable should call `send()` or `trySend()`", explanation = "If the rxFlowable trailing lambda does not call `send()` or `trySend()`, the returned Flowable will never emit!", category = Category.CORRECTNESS, priority = 2, severity = Severity.INFORMATIONAL, implementation = sourceImplementation(), ) val issues = listOf(ISSUE_RX_OBSERVABLE_DOES_NOT_EMIT, ISSUE_RX_FLOWABLE_DOES_NOT_EMIT) private val functionToIssue = mapOf( "rxObservable" to ISSUE_RX_OBSERVABLE_DOES_NOT_EMIT, "rxFlowable" to ISSUE_RX_FLOWABLE_DOES_NOT_EMIT, ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/rx/RxSubscribeOnMainDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.rx import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.lang.java.JavaLanguage import com.intellij.psi.PsiMethod import com.intellij.psi.PsiVariable import kotlin.reflect.full.safeCast import org.jetbrains.kotlin.asJava.elements.KtLightField import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UExpression import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.UastFacade import org.jetbrains.uast.java.JavaUCallExpression import org.jetbrains.uast.java.JavaUCompositeQualifiedExpression import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression import org.jetbrains.uast.kotlin.psi.UastKotlinPsiVariable import slack.lint.util.sourceImplementation /** * [Detector] for usages of `Observable.subscribeOn(AndroidSchedulers.mainThread())`. Typically, * this is not desired and instead users are looking for * `observeOn(AndroidSchedulers.mainThread())`. */ class RxSubscribeOnMainDetector : Detector(), SourceCodeScanner { companion object { private fun Implementation.toIssue() = Issue.create( id = "SubscribeOnMain", briefDescription = "subscribeOn called with the main thread scheduler.", explanation = """ Calling `subscribeOn(AndroidSchedulers.mainThread())` will cause the code ran at subscription time to be executed \ on the main thread - that is, code above this line. Typically this is not actually desired, and instead you want to use observeOn(AndroidSchedulers.mainThread()) \ which will cause the code below this line to be run on the main thread (eg the code inside your subscribe() \ block). """, category = Category.CORRECTNESS, priority = 4, severity = Severity.ERROR, implementation = this, ) val ISSUE = sourceImplementation().toIssue() } override fun getApplicableMethodNames(): List = listOf("subscribeOn") override fun getApplicableCallOwners() = listOf( "io/reactivex/rxjava3/core/Completable", "io/reactivex/rxjava3/core/Flowable", "io/reactivex/rxjava3/core/Maybe", "io/reactivex/rxjava3/core/Observable", "io/reactivex/rxjava3/core/Single", ) override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { val arg = node.valueArguments.first() when (arg) { is JavaUCompositeQualifiedExpression -> checkCall { JavaUCallExpression::class.safeCast(arg.selector) } is JavaUCallExpression -> checkCall { arg } is KotlinUQualifiedReferenceExpression -> checkCall { KotlinUFunctionCallExpression::class.safeCast(arg.selector) } is KotlinUFunctionCallExpression -> checkCall { arg } else -> checkVariable { arg } }.let { mainThreadFound -> if (mainThreadFound) { context.report( ISSUE, context.getCallLocation(node, includeReceiver = false, includeArguments = true), "This will make the code for the initial subscription (above this line) run on the main thread. " + "You probably want `observeOn(AndroidSchedulers.mainThread())`.", LintFix.create() .replace() .name("Replace with observeOn()") .text("subscribeOn") .with("observeOn") .build(), ) } } } /** * return true if the resolved [UCallExpression] has method name "mainThread" or * "immediateMainThread", false otherwise */ private fun checkCall(fn: () -> UCallExpression?): Boolean { return fn()?.let { call -> "mainThread" == call.methodName || "immediateMainThread" == call.methodName } ?: false } /** * return true if the resolved [UExpression] was created from the "mainThread" or * "immediateMainThread" methods, false otherwise */ private fun checkVariable(fn: () -> UExpression?): Boolean { return fn()?.let { exp -> when (exp.lang) { is KotlinLanguage -> checkKotlinVariable(exp) is JavaLanguage -> checkJavaVariable(exp) else -> return false } } ?: false } private fun checkKotlinVariable(exp: UExpression): Boolean { return when (exp) { is KotlinUSimpleReferenceExpression -> { val initializerText = when (val reference = exp.resolve()) { is KtLightField -> { // The variable reference is a member val initializer = (reference.kotlinOrigin as? KtProperty)?.initializer initializer?.node?.text } is UastKotlinPsiVariable -> { // The variable reference is local val initializer = reference.initializer initializer?.node?.text } else -> null } initializerText?.let { it.endsWith("mainThread()") || it.endsWith("immediateMainThread()") } ?: false } else -> false } } private fun checkJavaVariable(exp: UExpression): Boolean { val assignment: UCallExpression? = when (val variable = exp.sourcePsi?.reference?.resolve()) { // PsiVariable covers both PsiField and PsiLocalVariable is PsiVariable -> { ((UastFacade.getInitializerBody(variable) as? UQualifiedReferenceExpression)?.selector as? UCallExpression) } else -> null } val methodName = assignment?.resolve()?.name return methodName == "mainThread" || methodName == "immediateMainThread" } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/text/SpanMarkPointMissingMaskDetector.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.text import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector 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.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.UastLintUtils import org.jetbrains.uast.UBinaryExpression import org.jetbrains.uast.UExpression import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.UastBinaryOperator import org.jetbrains.uast.tryResolve import slack.lint.text.SpanMarkPointMissingMaskDetector.Companion.ISSUE import slack.lint.util.resolveQualifiedNameOrNull import slack.lint.util.sourceImplementation /** Checks for SpanMarkPointMissingMask. See [ISSUE]. */ class SpanMarkPointMissingMaskDetector : Detector(), SourceCodeScanner { companion object { val ISSUE = Issue.create( id = "SpanMarkPointMissingMask", briefDescription = "Check that Span flags use the bitwise mask SPAN_POINT_MARK_MASK when being compared to.", explanation = """ Spans flags can have priority or other bits set. \ Ensure that Span flags are checked using \ `currentFlag and Spanned.SPAN_POINT_MARK_MASK == desiredFlag` \ rather than just `currentFlag == desiredFlag` """, category = Category.CORRECTNESS, priority = 4, severity = Severity.ERROR, implementation = sourceImplementation(), ) } override fun getApplicableUastTypes() = listOf(UBinaryExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler = ReportingHandler(context) } private const val SPANNED_CLASS = "android.text.Spanned" private val MARK_POINT_FIELDS = setOf( "$SPANNED_CLASS.SPAN_INCLUSIVE_INCLUSIVE", "$SPANNED_CLASS.SPAN_INCLUSIVE_EXCLUSIVE", "$SPANNED_CLASS.SPAN_EXCLUSIVE_INCLUSIVE", "$SPANNED_CLASS.SPAN_EXCLUSIVE_EXCLUSIVE", ) private const val MASK_CLASS = "$SPANNED_CLASS.SPAN_POINT_MARK_MASK" /** Reports violations of SpanMarkPointMissingMask. */ private class ReportingHandler(private val context: JavaContext) : UElementHandler() { override fun visitBinaryExpression(node: UBinaryExpression) { if ( node.operator == UastBinaryOperator.EQUALS || node.operator == UastBinaryOperator.NOT_EQUALS || node.operator == UastBinaryOperator.IDENTITY_EQUALS || node.operator == UastBinaryOperator.IDENTITY_NOT_EQUALS ) { checkExpressions(node, node.leftOperand, node.rightOperand) checkExpressions(node, node.rightOperand, node.leftOperand) } } private fun checkExpressions( node: UBinaryExpression, markPointCheck: UExpression, maskCheck: UExpression, ) { if (markPointCheck.isMarkPointFieldName() && !maskCheck.isMaskClass()) { context.report( ISSUE, context.getLocation(node), """ Do not check against ${markPointCheck.sourcePsi?.text} directly. \ Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. """ .trimIndent(), LintFix.create() .replace() .name("Use bitwise mask") .text(maskCheck.sourcePsi?.text) .with("((${maskCheck.sourcePsi?.text}) and $MASK_CLASS)") .build(), ) } } } private fun UExpression.isMarkPointFieldName(): Boolean = this.getQualifiedName() in MARK_POINT_FIELDS private fun UExpression.getQualifiedName(): String? { return (this as? UReferenceExpression) ?.referenceNameElement ?.uastParent ?.tryResolve() ?.let(UastLintUtils::getQualifiedName) } private fun UExpression.isMaskClass(): Boolean { return if (this is UBinaryExpression) { this.leftOperand.resolveQualifiedNameOrNull() == MASK_CLASS || this.rightOperand.resolveQualifiedNameOrNull() == MASK_CLASS } else { false } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/ui/DoNotCallViewToString.kt ================================================ // Copyright (C) 2024 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.ui import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.util.InheritanceUtil import org.jetbrains.uast.UCallExpression import slack.lint.util.sourceImplementation class DoNotCallViewToString : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { if (node.methodName != "toString") return val method = node.resolve() ?: return val containingClass = method.containingClass ?: return if (InheritanceUtil.isInheritor(containingClass, "android.view.View")) { context.report( ISSUE, node, context.getNameLocation(node), "Do not call `View.toString()`", ) } } } } companion object { val ISSUE: Issue = Issue.Companion.create( "DoNotCallViewToString", "Do not use `View.toString()`", """ `View.toString()` and its overrides can often print surprisingly detailed information about \ the current view state, and has led to PII logging issues in the past. """, Category.Companion.SECURITY, 9, Severity.ERROR, sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/ui/ItemDecorationViewBindingDetector.kt ================================================ // Copyright (C) 2024 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.ui import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiClass import com.intellij.psi.util.InheritanceUtil.isInheritor import kotlin.jvm.java import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.getContainingUClass import slack.lint.util.implements import slack.lint.util.sourceImplementation /** * Lint detector that ensures `ItemDecoration` does not inflate a view. If the view contains * `TextView`, this textual information cannot be announced to screen readers by TalkBack. */ class ItemDecorationViewBindingDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler = object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val containingClass = node.getContainingUClass() ?: return if (!containingClass.implements(ITEM_DECORATION_CLASS_NAME)) return val method = node.resolve() ?: return val methodClass = method.containingClass if ( LAYOUT_INFLATER_METHOD_NAME == method.name && methodClass?.isViewBindingClass() == true ) { context.report(ISSUE, node, context.getNameLocation(node), ISSUE_DESCRIPTION) } } } private fun PsiClass.isViewBindingClass(): Boolean { return LAYOUT_INFLATER_PACKAGE_NAME == this.qualifiedName || isInheritor(this, VIEW_BINDING_PACKAGE_NAME) } companion object { private const val ISSUE_ID = "InflationInItemDecoration" private const val ITEM_DECORATION_CLASS_NAME = "androidx.recyclerview.widget.RecyclerView.ItemDecoration" private const val LAYOUT_INFLATER_METHOD_NAME = "inflate" private const val LAYOUT_INFLATER_PACKAGE_NAME = "android.view.LayoutInflater" private const val VIEW_BINDING_PACKAGE_NAME = "androidx.viewbinding.ViewBinding" private const val ISSUE_BRIEF_DESCRIPTION = "Avoid inflating a view to display text" private const val ISSUE_DESCRIPTION = """ ViewBinding should not be used in `ItemDecoration`. If an inflated view contains \ meaningful textual information, it will not be visible to TalkBack. This means \ that screen reader users will not be able to know what is on the screen. """ val ISSUE = Issue.create( id = ISSUE_ID, briefDescription = ISSUE_BRIEF_DESCRIPTION, explanation = ISSUE_DESCRIPTION, category = Category.A11Y, priority = 10, severity = Severity.WARNING, implementation = sourceImplementation(), ) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/BooleanLintOption.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.android.tools.lint.client.api.Configuration import com.android.tools.lint.detector.api.BooleanOption open class BooleanLintOption(private val option: BooleanOption) : LintOption { var value: Boolean = option.defaultValue private set override fun load(configuration: Configuration) { value = option.getValue(configuration) } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/LintOption.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.android.tools.lint.client.api.Configuration /** * A layer of indirection for implementations of option loaders without needing to extend from * Detector. This goes along with [OptionLoadingDetector]. */ interface LintOption { fun load(configuration: Configuration) } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/LintUtils.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.android.tools.lint.client.api.Configuration import com.android.tools.lint.client.api.JavaEvaluator import com.android.tools.lint.client.api.TYPE_BOOLEAN_WRAPPER import com.android.tools.lint.client.api.TYPE_BYTE_WRAPPER import com.android.tools.lint.client.api.TYPE_CHARACTER_WRAPPER import com.android.tools.lint.client.api.TYPE_DOUBLE_WRAPPER import com.android.tools.lint.client.api.TYPE_FLOAT_WRAPPER import com.android.tools.lint.client.api.TYPE_INTEGER_WRAPPER import com.android.tools.lint.client.api.TYPE_LONG_WRAPPER import com.android.tools.lint.client.api.TYPE_SHORT_WRAPPER import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.LintFix import com.android.tools.lint.detector.api.ResourceXmlDetector import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.StringOption import com.android.tools.lint.detector.api.UastLintUtils import com.android.tools.lint.detector.api.isKotlin import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement import com.intellij.psi.PsiJavaFile import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifierListOwner import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes import com.intellij.psi.PsiWildcardType import java.util.EnumSet import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.uast.UClass import org.jetbrains.uast.UExpression import org.jetbrains.uast.UMethod import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.USimpleNameReferenceExpression import org.jetbrains.uast.tryResolve /** * @param qualifiedName the qualified name of the desired interface type * @param nameFilter an optional name filter, used to check when to stop searching up the type * hierarchy. This is useful if you want to only check direct implementers in certain packages. * Called with a fully qualified class name; return false if you want to stop searching up the * type tree, true to continue. */ internal fun PsiClass.implements( qualifiedName: String, nameFilter: (String) -> Boolean = { true }, ): Boolean { val fqcn = this.qualifiedName ?: return false if (fqcn == qualifiedName) { // Found a match return true } if (!nameFilter(fqcn)) { // Don't proceed further return false } return this.superTypes.filterNotNull().any { classType -> classType.resolve()?.implements(qualifiedName, nameFilter) ?: false } } /** @return whether [owner] is a Kotlin `value` class. */ internal fun JavaEvaluator.isValueClass(owner: PsiModifierListOwner?): Boolean { // Check the annotation for JvmInline as a shorter check return owner?.hasAnnotation("kotlin.jvm.JvmInline") == true || hasModifier(owner, KtTokens.VALUE_KEYWORD) } internal fun UClass.isInnerClass(evaluator: JavaEvaluator): Boolean { // If it has no containing class, it's top-level and therefore not inner containingClass ?: return false // If it's static (i.e. in Java), it's not an inner class if (isStatic) return false // If it's Kotlin and "inner", then it's definitely an inner class if (isKotlin(language) && evaluator.hasModifier(this, KtTokens.INNER_KEYWORD)) return true // We could check the containing class's innerClasses to look for a match here, but we've // logically ruled // out this possibility above return false } @Suppress("SpreadOperator") internal inline fun sourceImplementation( shouldRunOnTestSources: Boolean = true ): Implementation where T : Detector, T : SourceCodeScanner { // We use the overloaded constructor that takes a varargs of `Scope` as the last param. // This is to enable on-the-fly IDE checks. We are telling lint to run on both // JAVA and TEST_SOURCES in the `scope` parameter but by providing the `analysisScopes` // params, we're indicating that this check can run on either JAVA or TEST_SOURCES and // doesn't require both of them together. // From discussion on lint-dev https://groups.google.com/d/msg/lint-dev/ULQMzW1ZlP0/1dG4Vj3-AQAJ // This was supposed to be fixed in AS 3.4 but still required as recently as 3.6-alpha10. return if (shouldRunOnTestSources) { Implementation( T::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES), EnumSet.of(Scope.JAVA_FILE), EnumSet.of(Scope.TEST_SOURCES), ) } else { Implementation(T::class.java, EnumSet.of(Scope.JAVA_FILE)) } } @Suppress("SpreadOperator") internal inline fun resourcesImplementation(): Implementation { return Implementation(T::class.java, Scope.RESOURCE_FILE_SCOPE) } /** Removes a given [node] as a fix. */ internal fun LintFix.Builder.removeNode( context: JavaContext, node: PsiElement, name: String? = null, autoFix: Boolean = true, text: String = node.text, ): LintFix { val fixName = name ?: "Remove '$text'" return replace() .name(fixName) .range(context.getLocation(node)) .shortenNames() .text(text) .with("") .apply { if (autoFix) { autoFix() } } .build() } internal fun String.snakeToCamel(): String { return buildString { var capNext = false var letterSeen = false for (c in this@snakeToCamel) { if (c == '_' || c == '-') { capNext = letterSeen continue } else { letterSeen = true if (capNext) { append(c.uppercaseChar()) capNext = false } else { append(c) } } } } } @Suppress("ComplexCondition") internal fun String.toScreamingSnakeCase(): String { return buildString { var prevWasLower = false var hasPendingUnderScore = false var letterSeen = false for (c in this@toScreamingSnakeCase) { if (c == '_') { if (letterSeen) { hasPendingUnderScore = true } continue } else if (c == '-' || c == '.' || c == ':' || c == '/') { // Wild west characters in enum member names // TODO maybe we should report these if (letterSeen) { hasPendingUnderScore = true } } else { letterSeen = true if (hasPendingUnderScore) { append('_') } hasPendingUnderScore = false if (c.isUpperCase()) { if (prevWasLower) { prevWasLower = false append('_') } append(c) } else { prevWasLower = true append(c.uppercaseChar()) } } } } } /** List of platform types that Moshi's reflective adapter refuses. From ClassJsonAdapter. */ private val PLATFORM_PACKAGES = setOf("android", "androidx", "java", "javax", "kotlin", "kotlinx", "scala") private val BOXED_PRIMITIVES = setOf( TYPE_INTEGER_WRAPPER, TYPE_BOOLEAN_WRAPPER, TYPE_BYTE_WRAPPER, TYPE_SHORT_WRAPPER, TYPE_LONG_WRAPPER, TYPE_DOUBLE_WRAPPER, TYPE_FLOAT_WRAPPER, TYPE_CHARACTER_WRAPPER, ) internal fun PsiClass.isBoxedPrimitive(): Boolean { val fqcn = qualifiedName ?: return false return fqcn in BOXED_PRIMITIVES } internal fun PsiClass.isString(): Boolean { val fqcn = qualifiedName ?: return false return fqcn == "java.lang.String" } internal fun PsiClass.isObjectOrAny(): Boolean { val fqcn = qualifiedName ?: return false return fqcn == "java.lang.Object" } internal fun PsiClass.isPlatformType(): Boolean { val fqcn = qualifiedName ?: return false val firstPackagePart = fqcn.substringBefore(".") return firstPackagePart in PLATFORM_PACKAGES } /** * Given reference expressions, try to unwrap the simple name expression (useful if the reference is * always the same type, like an enum). * * `Foo.BAR` -> BAR `BAR` -> BAR */ internal fun UExpression.unwrapSimpleNameReferenceExpression(): USimpleNameReferenceExpression { return when (this) { is USimpleNameReferenceExpression -> this is UQualifiedReferenceExpression -> this.selector.unwrapSimpleNameReferenceExpression() else -> error("Unrecognized reference expression type $javaClass") } } /** * Returns the fully qualified name of the expression, or null if unknown. * * For example, given: * ``` * import org.x.Clazz.CONSTANT * ... * if (aVar == CONSTANT) * ^^^^^^^^ * ``` * * The qualified name of the underlined expression will be "org.x.Clazz.CONSTANT". */ internal fun UExpression.resolveQualifiedNameOrNull(): String? { return (this as? UReferenceExpression)?.referenceNameElement?.uastParent?.tryResolve()?.let { UastLintUtils.getQualifiedName(it) } } /** * Collects the return type of this [UMethod] in a suspend-safe way. * * For coroutines, the suspend methods return context rather than the source-declared return type, * which is encoded in a continuation parameter at the end of the parameter list. * * For example, the following snippet: * ``` * suspend fun foo(): String * ``` * * Will appear like so to lint: * ``` * Object foo(Continuation continuation) * ``` */ internal fun UMethod.safeReturnType(context: JavaContext): PsiType? { if (language == KotlinLanguage.INSTANCE && context.evaluator.isSuspend(this)) { val classReference = parameterList.parameters.lastOrNull()?.type as? PsiClassType ?: return null val wildcard = classReference.parameters.singleOrNull() as? PsiWildcardType ?: return null return wildcard.bound } else { return returnType } } /** Loads a [StringOption] as a [delimiter]-delimited [Set] of strings. */ internal fun StringOption.loadAsSet( configuration: Configuration, delimiter: String = ",", ): Set { return getValue(configuration) ?.splitToSequence(delimiter) .orEmpty() .map(String::trim) .filter(String::isNotBlank) .toSet() } internal inline fun Array.mapArray(transform: (T) -> R): Array = Array(this.size) { i -> transform(this[i]) } internal inline fun measureTimeMillisWithResult(block: () -> T): Pair { val start = System.currentTimeMillis() val result = block() return Pair(System.currentTimeMillis() - start, result) } private val logVerbosely by lazy { System.getProperty("slack.lint.logVerbosely", "false").toBoolean() } private val logErrorsVerbosely by lazy { System.getProperty("slack.lint.logErrorsVerbosely", "true").toBoolean() } /** * Logs to std if [logVerbosely] is enabled. Useful for debugging and should not generally be * enabled. */ internal fun slackLintLog(message: String) { if (logVerbosely) { println("SlackLint: $message") } } /** * Logs to std if [logErrorsVerbosely] is enabled. Important for errors that you don't necessarily * want to fail the build */ internal fun slackLintErrorLog(message: String) { if (logErrorsVerbosely) { System.err.println("SlackLint: $message") } } /** Returns whether [this] has [packageName] as its package name. */ internal fun PsiMethod.isInPackageName(packageName: PackageName): Boolean { val actual = (containingFile as? PsiJavaFile)?.packageName return packageName.javaPackageName == actual } /** Whether this [PsiMethod] returns Unit */ internal val PsiMethod.returnsUnit get() = returnType.isVoidOrUnit /** * Whether this [PsiType] is `void` or [Unit] * * In Kotlin 1.6 some expressions now explicitly return [Unit] instead of just being [PsiType.VOID], * so this returns whether this type is either. */ internal val PsiType?.isVoidOrUnit get() = this == PsiTypes.voidType() || this?.canonicalText == "kotlin.Unit" ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/MetadataJavaEvaluator.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.android.tools.lint.client.api.JavaEvaluator import com.android.tools.lint.model.LintModelDependencies import com.intellij.lang.jvm.JvmClassKind import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiArrayInitializerMemberValue import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import com.intellij.psi.PsiCompiledElement import com.intellij.psi.PsiElement import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiModifierListOwner import com.intellij.psi.PsiPackage import com.intellij.psi.PsiType import java.util.Optional import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.optionals.getOrNull import kotlin.metadata.ClassKind import kotlin.metadata.KmClass import kotlin.metadata.Modality import kotlin.metadata.isData import kotlin.metadata.isValue import kotlin.metadata.jvm.JvmMetadataVersion import kotlin.metadata.jvm.KotlinClassMetadata import kotlin.metadata.jvm.Metadata as MetadataWithNullableArgs import kotlin.metadata.kind import kotlin.metadata.modality import org.jetbrains.kotlin.lexer.KtModifierKeywordToken import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.findContaining import org.jetbrains.uast.toUElementOfType /** * A delegating [JavaEvaluator] that implements more comprehensive checks for Kotlin classes via * metadata annotations. * * This is important because, when `checkDependencies` is set to false, Lint detectors cannot see * Kotlin language features in externally-compiled elements. This means that constructs like `data * classes` or similar are not visible. Using kotlinx-metadata, we can parse the [Metadata] * annotations on the containing classes and read these language features from them. * * This is necessary due to https://issuetracker.google.com/issues/283654244. */ class MetadataJavaEvaluator(private val file: String, private val delegate: JavaEvaluator) : JavaEvaluator() { private companion object { // Not an exhaustive list, but at least the ones we look at currently private val KOTLIN_METADATA_TOKENS = mapOf( KtTokens.DATA_KEYWORD to TokenData { it.isData }, KtTokens.SEALED_KEYWORD to TokenData(applicableClassKinds = setOf(JvmClassKind.CLASS, JvmClassKind.INTERFACE)) { it.modality == Modality.SEALED }, KtTokens.OBJECT_KEYWORD to TokenData { it.kind == ClassKind.OBJECT }, KtTokens.COMPANION_KEYWORD to TokenData { it.kind == ClassKind.COMPANION_OBJECT }, KtTokens.VALUE_KEYWORD to TokenData { it.isValue }, ) } private data class TokenData( val applicableClassKinds: Set = setOf(JvmClassKind.CLASS), val isApplicable: (KmClass) -> Boolean, ) /** Flag to disable as needed. */ private val checkMetadata = System.getProperty("slack.lint.checkMetadata", "true").toBoolean() private val cachedClasses = ConcurrentHashMap>() // region Delegating functions override val dependencies: LintModelDependencies? get() = delegate.dependencies override fun extendsClass(cls: PsiClass?, className: String, strict: Boolean): Boolean = delegate.extendsClass(cls, className, strict) @Suppress("DEPRECATION") @Deprecated( "Use getAnnotation returning a UAnnotation instead", replaceWith = ReplaceWith("getAnnotation(listOwner, *annotationNames)"), ) override fun findAnnotation( listOwner: PsiModifierListOwner?, vararg annotationNames: String, ): PsiAnnotation? = delegate.findAnnotation(listOwner, *annotationNames) @Suppress("DEPRECATION") @Deprecated( "Use getAnnotationInHierarchy returning a UAnnotation instead", replaceWith = ReplaceWith("getAnnotationInHierarchy(listOwner, *annotationNames)"), ) override fun findAnnotationInHierarchy( listOwner: PsiModifierListOwner, vararg annotationNames: String, ): PsiAnnotation? = delegate.findAnnotationInHierarchy(listOwner, *annotationNames) override fun findClass(qualifiedName: String): PsiClass? = delegate.findClass(qualifiedName) override fun findJarPath(element: PsiElement): String? = delegate.findJarPath(element) override fun findJarPath(element: UElement): String? = delegate.findJarPath(element) @Suppress("DEPRECATION") @Deprecated( "Use getAnnotations() instead; consider providing a parent", replaceWith = ReplaceWith("getAnnotations(owner, inHierarchy)"), ) override fun getAllAnnotations( owner: PsiModifierListOwner, inHierarchy: Boolean, ): Array = delegate.getAllAnnotations(owner, inHierarchy) override fun getAllAnnotations(owner: UAnnotated, inHierarchy: Boolean): List = delegate.getAllAnnotations(owner, inHierarchy) override fun getAnnotation( listOwner: PsiModifierListOwner?, vararg annotationNames: String, ): UAnnotation? = delegate.getAnnotation(listOwner, *annotationNames) override fun getAnnotationInHierarchy( listOwner: PsiModifierListOwner, vararg annotationNames: String, ): UAnnotation? = delegate.getAnnotationInHierarchy(listOwner, *annotationNames) override fun getAnnotations( owner: PsiModifierListOwner?, inHierarchy: Boolean, parent: UElement?, ): List = delegate.getAnnotations(owner, inHierarchy, parent) override fun getClassType(psiClass: PsiClass?): PsiClassType? = delegate.getClassType(psiClass) override fun getPackage(node: PsiElement): PsiPackage? = delegate.getPackage(node) override fun getPackage(node: UElement): PsiPackage? = delegate.getPackage(node) override fun getTypeClass(psiType: PsiType?): PsiClass? = delegate.getTypeClass(psiType) override fun implementsInterface(cls: PsiClass, interfaceName: String, strict: Boolean): Boolean = delegate.implementsInterface(cls, interfaceName, strict) // endregion /** Deep isObject check that checks if the given [cls] is an `object` class. */ fun isObject(cls: PsiClass?): Boolean { if (cls == null) return false (cls as? UClass ?: cls.toUElementOfType())?.let { uClass -> if (uClass.sourcePsi is KtObjectDeclaration) { return true } else if (canCheckMetadata(cls)) { val (applicableClassKinds, isApplicable) = KOTLIN_METADATA_TOKENS.getValue(KtTokens.OBJECT_KEYWORD) if (uClass.classKind in applicableClassKinds) { uClass.getOrParseMetadata()?.let { kmClass -> return isApplicable(kmClass) } } } } return false } private fun canCheckMetadata(element: PsiElement): Boolean { return checkMetadata && element is PsiCompiledElement } override fun hasModifier(owner: PsiModifierListOwner?, keyword: KtModifierKeywordToken): Boolean { val superValue = super.hasModifier(owner, keyword) // If it's not a compiled element or not a PsiClass, trust the super value and move on if (owner !is PsiClass || !canCheckMetadata(owner)) { return superValue } // We're working with an externally compiled element and it's a PsiClass, so we can do more // thorough checks here. KOTLIN_METADATA_TOKENS[keyword]?.let { (applicableClassKinds, isApplicable) -> owner.findContaining(UClass::class.java)?.let { cls -> // Only parse if the target class kind is applicable to the token we're checking. For // example - when checking `data` tokens, they're not applicable to interfaces or enums. if (cls.classKind in applicableClassKinds) { cls.getOrParseMetadata()?.let { kmClass -> return isApplicable(kmClass) } } } } return superValue } private fun UAnnotated.getOrParseMetadata(): KmClass? { val cls = when (this) { is UClass -> this else -> return null // Only classes are supported right now } val fqcn = qualifiedName ?: run { slackLintErrorLog("Qualified name is null for $cls in file $file") return null } return cachedClasses // Don't use getOrPut. Kotlin's extension may still invoke the body and we don't want that .computeIfAbsent(fqcn) { key -> val annotation = cls.findAnnotation("kotlin.Metadata") ?: return@computeIfAbsent Optional.empty() val (durationMillis, metadata) = measureTimeMillisWithResult { annotation.parseMetadata(key) } slackLintLog("Took ${durationMillis}ms to parse metadata for $key.") Optional.ofNullable(metadata) } .getOrNull() } private fun UAnnotation.parseMetadata(classNameHint: String): KmClass? { val parsedMetadata = try { KotlinClassMetadata.readStrict(toMetadataAnnotation()) } catch (e: IllegalArgumentException) { try { KotlinClassMetadata.readLenient(toMetadataAnnotation()).also { slackLintErrorLog( "Could not load metadata for $classNameHint from file $file with strict parsing. Using lenient parsing." ) } } catch (e: IllegalArgumentException) { // Extremely weird case, log this specifically slackLintErrorLog( "Could not load metadata for $classNameHint from file $file. This usually happens if the Kotlin version the class was compiled against is too new for lint to read (${JvmMetadataVersion.LATEST_STABLE_SUPPORTED})." ) return null } } return when (parsedMetadata) { is KotlinClassMetadata.Class -> { parsedMetadata.kmClass.also { slackLintLog("Loaded KmClass for $classNameHint from file $file") } } else -> { slackLintLog( """ Could not load KmClass for $classNameHint from file $file. Metadata was $parsedMetadata """ .trimIndent() ) null } } } private fun UAnnotation.toMetadataAnnotation(): Metadata { return MetadataWithNullableArgs( kind = findAttributeValue("k")?.parseIntMember(), metadataVersion = findAttributeValue("mv")?.parseIntArray(), data1 = findAttributeValue("d1")?.parseStringArray(), data2 = findAttributeValue("d2")?.parseStringArray(), extraString = findAttributeValue("xs")?.parseStringMember(), packageName = findAttributeValue("pn")?.parseStringMember(), extraInt = findAttributeValue("xi")?.parseIntMember(), ) } private val PsiLiteralExpression.intValue: Int get() = stringValue.toInt() private val PsiLiteralExpression.stringValue: String get() = value.toString() private fun UExpression.parseIntMember() = (sourcePsi as PsiLiteralExpression).intValue private fun UExpression.parseStringMember() = (sourcePsi as PsiLiteralExpression).stringValue private fun UExpression.parseStringArray() = (sourcePsi as PsiArrayInitializerMemberValue).initializers.mapArray { value -> (value as PsiLiteralExpression).stringValue } private fun UExpression.parseIntArray(): IntArray { val initializers = (sourcePsi as PsiArrayInitializerMemberValue).initializers return IntArray(initializers.size) { index -> (initializers[index] as PsiLiteralExpression).intValue } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/Names.kt ================================================ /* * Copyright 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package slack.lint.util import kotlin.metadata.ClassName /** * Represents a qualified package * * @property segments the segments representing the package */ internal class PackageName(val segments: List) { /** The Java-style package name for this [Name], separated with `.` */ val javaPackageName: String get() = segments.joinToString(".") } /** * Represents the qualified name for an element * * @property pkg the package for this element * @property nameSegments the segments representing the element - there can be multiple in the case * of nested classes. */ internal class Name(private val pkg: PackageName, private val nameSegments: List) { /** The short name for this [Name] */ val shortName: String get() = nameSegments.last() /** The Java-style fully qualified name for this [Name], separated with `.` */ val javaFqn: String get() = pkg.segments.joinToString(".", postfix = ".") + nameSegments.joinToString(".") /** * The [ClassName] for use with kotlinx.metadata. Note that in kotlinx.metadata the actual type * might be different from the underlying JVM type, for example: kotlin/Int -> java/lang/Integer */ val kmClassName: ClassName get() = pkg.segments.joinToString("/", postfix = "/") + nameSegments.joinToString(".") /** The [PackageName] of this element. */ val packageName: PackageName get() = pkg } /** @return a [PackageName] with a Java-style (separated with `.`) [packageName]. */ internal fun Package(packageName: String): PackageName = PackageName(packageName.split(".")) /** @return a [PackageName] with a Java-style (separated with `.`) [packageName]. */ internal fun Package(packageName: PackageName, shortName: String): PackageName = PackageName(packageName.segments + shortName.split(".")) /** @return a [Name] with the provided [pkg] and Java-style (separated with `.`) [shortName]. */ internal fun Name(pkg: PackageName, shortName: String): Name = Name(pkg, shortName.split(".")) ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/OptionLoadingDetector.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.android.tools.lint.detector.api.Context import com.android.tools.lint.detector.api.Detector /** A [Detector] that supports reading the given [options]. */ abstract class OptionLoadingDetector(vararg options: LintOption) : Detector() { private val options = options.toList() override fun beforeCheckRootProject(context: Context) { super.beforeCheckRootProject(context) val config = context.configuration options.forEach { it.load(config) } } } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/Priorities.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util /** * Priorities with semantic names. Partially for readability, partially so detekt stops nagging * about MagicNumber. */ object Priorities { const val HIGH = 10 const val NORMAL = 5 const val LOW = 3 const val NONE = 1 } ================================================ FILE: slack-lint-checks/src/main/java/slack/lint/util/StringSetLintOption.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.android.tools.lint.client.api.Configuration import com.android.tools.lint.detector.api.StringOption open class StringSetLintOption(private val option: StringOption) : LintOption { var value: Set = emptySet() private set override fun load(configuration: Configuration) { value = option.loadAsSet(configuration) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/AlwaysNullReadOnlyVariableDetectorTest.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class AlwaysNullReadOnlyVariableDetectorTest : BaseSlackLintTest() { override fun getDetector() = AlwaysNullReadOnlyVariableDetector() override fun getIssues() = AlwaysNullReadOnlyVariableDetector.ISSUES.toList() @Test fun `initializing a read-only variable with null shows warnings`() { lint() .files( kotlin( """ class Test { val str: String? = null fun method() { val strInFunction: String? = null } } """ ) .indented() ) .run() .expect( """ src/Test.kt:2: Warning: Avoid initializing read-only variable with null in Kotlin [AvoidNullInitForReadOnlyVariables] val str: String? = null ~~~~ src/Test.kt:5: Warning: Avoid initializing read-only variable with null in Kotlin [AvoidNullInitForReadOnlyVariables] val strInFunction: String? = null ~~~~ 0 errors, 2 warnings """ .trimIndent() ) } @Test fun `returning null in custom getter shows warnings`() { lint() .files( kotlin( """ class Test { val str1: String? get() = null val str2: String? get() { return null } val str3: String? = null get() = field } """ ) .indented() ) .run() .expect( """ src/Test.kt:10: Warning: Avoid initializing read-only variable with null in Kotlin [AvoidNullInitForReadOnlyVariables] val str3: String? = null ~~~~ src/Test.kt:3: Warning: Avoid returning null in getter for read-only properties in Kotlin [AvoidReturningNullInGetter] get() = null ~~~~ src/Test.kt:7: Warning: Avoid returning null in getter for read-only properties in Kotlin [AvoidReturningNullInGetter] return null ~~~~ 0 errors, 3 warnings """ .trimIndent() ) } @Test fun `initializing a read-write variable with null doesn't show warnings`() { lint() .files( kotlin( """ class Test { var str: String? = null fun method() { var str: String = null } } """ ) .indented() ) .run() .expectClean() } @Test fun `initializing a read-only variable with not null doesn't show warnings`() { lint() .files( kotlin( """ class Test { val str: String? = "str" fun method() { val str: String? = "str" } } """ ) .indented() ) .run() .expectClean() } @Test fun `initializing a read-write variable with not null doesn't show warnings`() { lint() .files( kotlin( """ class Test { var str: String? = "str" fun method() { var str: String? = "str" } } """ ) .indented() ) .run() .expectClean() } @Test fun `returning not null in custom getter doesn't show warnings`() { lint() .files( kotlin( """ class Test { val str1: String? get() = "str" val str2: String? get() { return "str" } } """ ) .indented() ) .run() .expectClean() } @Test fun `parameter properties initialized to null are ok`() { lint() .files( kotlin( """ class Test(val str1: String? = null) """ ) .indented() ) .run() .expectClean() } @Test fun `open properties initialized to null are ok`() { lint() .files( kotlin( """ open class Test { open val str1: String? = null } """ ) .indented() ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/ArgInFormattedQuantityStringResDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test class ArgInFormattedQuantityStringResDetectorTest : BaseSlackLintTest() { override val skipTestModes: Array = arrayOf(TestMode.PARENTHESIZED) override fun getDetector() = ArgInFormattedQuantityStringResDetector() override fun getIssues() = ArgInFormattedQuantityStringResDetector.issues.toList() @Test fun getQuantityStringWithNoLocalizedFormatTest() { lint() .files( java( "" + "package com.slack.lint;\n" + "\n" + "import android.content.res.Resources;\n" + "\n" + "public class Foo {\n" + " public void bar(Resources res) {\n" + " String s = res.getQuantityString(0, 3, 3, \"asdf\");\n" + " }\n" + "}\n" ) ) .run() .expect( ("src/com/slack/lint/Foo.java:7: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes]\n" + " String s = res.getQuantityString(0, 3, 3, \"asdf\");\n" + " ~\n" + "src/com/slack/lint/Foo.java:7: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes]\n" + " String s = res.getQuantityString(0, 3, 3, \"asdf\");\n" + " ~~~~~~\n" + "0 errors, 2 warnings\n") ) } @Test fun getQuantityStringWithNoLocalizedFormatTestKotlin() { lint() .files( kotlin( "" + "package com.slack.lint\n" + "\n" + "import android.content.res.Resources\n" + "\n" + "class Foo {\n" + " fun bar(res: Resources) {\n" + " val s = res.getQuantityString(0, 3, 3, \"asdf\")\n" + " }\n" + "}" ) ) .run() .expect( """ src/com/slack/lint/Foo.kt:7: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes] val s = res.getQuantityString(0, 3, 3, "asdf") ~ src/com/slack/lint/Foo.kt:7: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes] val s = res.getQuantityString(0, 3, 3, "asdf") ~~~~~~ 0 errors, 2 warnings """ .trimIndent() ) } @Test fun getQuantityStringWithLocalizedFormatTest() { lint() .files( java( ("" + "package com.slack.lint;\n" + "import static com.slack.lint.Foo.LocalizationUtils.getFormattedCount;\n" + "\n" + "import android.content.res.Resources;\n" + "\n" + "public class Foo {\n" + " public void bar(Resources res) {\n" + " String s = res.getQuantityString(0, 3, getFormattedCount(), 3);\n" + " }\n" + " public static class LocalizationUtils {\n" + " public static String getFormattedCount() { return \"\"; }\n" + " }" + "}\n") ) ) .run() .expect( "src/com/slack/lint/Foo.java:8: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes]\n" + " String s = res.getQuantityString(0, 3, getFormattedCount(), 3);\n" + " ~\n" + "0 errors, 1 warnings\n" ) } @Test fun getQuantityStringWithLocalizedFormatTestKotlin() { lint() .files( kotlin( "" + "package com.slack.lint\n" + "\n" + "import android.content.res.Resources\n" + "import com.slack.lint.Foo.LocalizationUtils.Companion.getFormattedCount\n" + "\n" + "class Foo {\n" + " fun bar(res: Resources) {\n" + " val s = res.getQuantityString(0, 3, getFormattedCount(), 3)\n" + " }\n" + "\n" + " class LocalizationUtils {\n" + " companion object {\n" + " fun getFormattedCount(): String {\n" + " return \"\"\n" + " }\n" + " }\n" + " }\n" + "}\n" ) ) .run() .expect( "src/com/slack/lint/Foo.kt:8: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes]\n" + " val s = res.getQuantityString(0, 3, getFormattedCount(), 3)\n" + " ~\n" + "0 errors, 1 warnings\n" ) } @Test fun getQuantityStringWithOutsideAssignmentLocalizedFormatTest() { lint() .files( java( ("" + "package com.slack.lint;\n" + "import static com.slack.lint.Foo.LocalizationUtils.getFormattedCount;\n" + "\n" + "import android.content.res.Resources;\n" + "\n" + "public class Foo {\n" + " public void bar(Resources res) {\n" + " final String a = getFormattedCount();\n" + " String s = res.getQuantityString(0, 3, a, 3);\n" + " }\n" + " public static class LocalizationUtils {\n" + " public static String getFormattedCount() { return \"\"; }\n" + " }\n" + "}\n") ) ) .run() .expect( "src/com/slack/lint/Foo.java:9: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes]\n" + " String s = res.getQuantityString(0, 3, a, 3);\n" + " ~\n" + "0 errors, 1 warnings\n" ) } @Test fun getQuantityStringWithOutsideAssignmentLocalizedFormatTestKotlin() { lint() .files( kotlin( "" + "package com.slack.lint\n" + "\n" + "import android.content.res.Resources\n" + "import com.slack.lint.Foo.LocalizationUtils.Companion.getFormattedCount\n" + "\n" + "class Foo {\n" + " fun bar(res: Resources) {\n" + " val s = res.getQuantityString(0, 3, getFormattedCount(), \"asdf\")\n" + " }\n" + "\n" + " class LocalizationUtils {\n" + " companion object {\n" + " fun getFormattedCount(): String {\n" + " return \"\"\n" + " }\n" + " }\n" + " }\n" + "}" ) ) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes] val s = res.getQuantityString(0, 3, getFormattedCount(), "asdf") ~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun getQuantityStringWithFullyQualifiedMethodName() { lint() .files( kotlin( "" + "package com.slack.lint\n" + "\n" + "import android.content.res.Resources\n" + "\n" + "class Foo {\n" + " fun bar(res: Resources) {\n" + " val s = res.getQuantityString(0, 3, LocalizationUtils.getFormattedCount(), \"asdf\")\n" + " }\n" + " \n" + " class LocalizationUtils {\n" + " companion object {\n" + " fun getFormattedCount(): String {\n" + " return \"\"\n" + " }\n" + " }\n" + " }\n" + "}" ) ) .run() .expect( """ src/com/slack/lint/Foo.kt:7: Warning: This may require a localized count modifier. If so, use LocalizationUtils.getFormattedCount(). Consult #plz-localization if you are unsure. [ArgInFormattedQuantityStringRes] val s = res.getQuantityString(0, 3, LocalizationUtils.getFormattedCount(), "asdf") ~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun getQuantityStringNoFormatArgsTest() { lint() .files( java( "" + "package com.slack.lint;\n" + "\n" + "import android.content.res.Resources;\n" + "\n" + "public class Foo {\n" + " public void bar(Resources res) {\n" + " String s = res.getQuantityString(0, 3);\n" + " }\n" + "}\n" ) ) .run() .expectClean() } @Test fun getQuantityStringNoFormatArgsTestKotlin() { lint() .files( kotlin( "" + "package com.slack.lint\n" + "\n" + "import android.content.res.Resources\n" + "\n" + "class Foo {\n" + " fun bar(res: Resources) {\n" + " val s = res.getQuantityString(0, 3)\n" + " }\n" + "}" ) ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/BaseSlackLintTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.checks.infrastructure.TestMode import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.utils.SdkUtils import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.InputStream import java.net.MalformedURLException import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) abstract class BaseSlackLintTest : LintDetectorTest() { private val rootPath = "resources/test/com/slack/lint/data/" private val stubsPath = "testStubs" /** * Lint periodically adds new "TestModes" to LintDetectorTest. These modes act as a sort of chaos * testing mechanism, adding different common variations of code (extra spaces, extra parens, etc) * to the test files to ensure that lints are robust against them. They also make it quite * difficult to test against and need extra work sometimes to properly support, so we expose this * property to allow tests to skip certain modes that need more work in subsequent PRs. */ open val skipTestModes: Array? = null fun loadStub(stubName: String): TestFile { return copy(stubsPath + File.separatorChar + stubName, "src/main/java/$stubName") } abstract override fun getDetector(): Detector abstract override fun getIssues(): List override fun lint(): TestLintTask { val sdkLocation = System.getProperty("android.sdk") ?: System.getenv("ANDROID_HOME") val lintTask = super.lint() sdkLocation?.let { lintTask.sdkHome(File(it)) } lintTask.allowCompilationErrors(false) skipTestModes?.let { testModesToSkip -> lintTask.skipTestModes(*testModesToSkip) } return lintTask } /** * The default finder for resources doesn't work with our file structure; this ensures it will. * * https://www.bignerdranch.com/blog/building-custom-lint-checks-in-android/ */ override fun getTestResource(relativePath: String, expectExists: Boolean): InputStream { val path = (rootPath + relativePath).replace('/', File.separatorChar) val file = File(getTestDataRootDir(), path) if (file.exists()) { try { return BufferedInputStream(FileInputStream(file)) } catch (_: FileNotFoundException) { if (expectExists) { fail("Could not find file $relativePath") } } } return BufferedInputStream(ByteArrayInputStream("".toByteArray())) } private fun getTestDataRootDir(): File? { val source = javaClass.protectionDomain.codeSource if (source != null) { val location = source.location try { val classesDir = SdkUtils.urlToFile(location) return classesDir.parentFile!!.absoluteFile.parentFile!!.parentFile } catch (e: MalformedURLException) { fail(e.localizedMessage) } } return null } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/CircuitScreenDataClassDetectorTest.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test class CircuitScreenDataClassDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = CircuitScreenDataClassDetector() override fun getIssues(): List = listOf(CircuitScreenDataClassDetector.ISSUE) @Test fun `Test CircuitScreenDataClassDetector - regular class implements Screen - fails`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen @Parcelize class HomeScreen(val userId: String) : Screen { data class State(message: String) : State } """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( """ src/com/example/screens/HomeScreen.kt:6: Error: ${CircuitScreenDataClassDetector.MESSAGE} [${CircuitScreenDataClassDetector.ISSUE_ID}] class HomeScreen(val userId: String) : Screen { ~~~~~ 1 error """ .trimIndent() ) } @Test fun `Test CircuitScreenDataClassDetector - data class implements Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen data class HomeScreen(val userId: String) : Screen """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - data object implements Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen data object SettingsScreen : Screen """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - interface extends Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen interface CustomScreen : Screen { val id: String } """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - regular class not implementing Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens class HomeViewModel(val userId: String) """ .trimIndent() ) .indented() lint().files(testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - class name contains 'class' keyword - only replaces declaration`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen class MyClassScreen(val id: String) : Screen """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( """ src/com/example/screens/MyClassScreen.kt:5: Error: ${CircuitScreenDataClassDetector.MESSAGE} [${CircuitScreenDataClassDetector.ISSUE_ID}] class MyClassScreen(val id: String) : Screen ~~~~~ 1 error """ .trimIndent() ) } @Test fun `Test CircuitScreenDataClassDetector - regular object class implements Screen - suggests data object`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen object SettingsScreen : Screen """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( """ src/com/example/screens/SettingsScreen.kt:5: Error: ${CircuitScreenDataClassDetector.MESSAGE} [${CircuitScreenDataClassDetector.ISSUE_ID}] object SettingsScreen : Screen ~~~~~~ 1 error """ .trimIndent() ) } @Test fun `Test CircuitScreenDataClassDetector - open class implements Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen open class ProfileScreen(val userId: String) : Screen """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - companion object implements Screen - fails`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen class SomeClass { companion object NavScreen : Screen } """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - nested class implements Screen - fails`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen class OuterClass { class NestedScreen(val id: String) : Screen } """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( """ src/com/example/screens/OuterClass.kt:6: Error: ${CircuitScreenDataClassDetector.MESSAGE} [${CircuitScreenDataClassDetector.ISSUE_ID}] class NestedScreen(val id: String) : Screen ~~~~~ 1 error """ .trimIndent() ) } @Test fun `Test CircuitScreenDataClassDetector - abstract class implements Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen abstract class BaseScreen : Screen { abstract val screenId: String } """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - sealed class implements Screen - fails`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen sealed class NavigationScreen : Screen { data class Home(val userId: String) : NavigationScreen() data object Settings : NavigationScreen() } """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } @Test fun `Test CircuitScreenDataClassDetector - inner class implements Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen class OuterClass { class InnerScreen(val id: String) : Screen } """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( "src/com/example/screens/OuterClass.kt:6: Error: Circuit Screen implementations should be data classes or data objects, not regular classes. [CircuitScreenShouldBeDataClass]\n" + " class InnerScreen(val id: String) : Screen\n" + " ~~~~~\n" + "1 error" ) } @Test fun `Test CircuitScreenDataClassDetector - class with no constructor parameters - suggests data object`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen class EmptyScreen : Screen """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( """ src/com/example/screens/EmptyScreen.kt:5: Error: ${CircuitScreenDataClassDetector.MESSAGE} [${CircuitScreenDataClassDetector.ISSUE_ID}] class EmptyScreen : Screen ~~~~~ 1 error """ .trimIndent() ) } @Test fun `Test CircuitScreenDataClassDetector - class with empty constructor - suggests data object`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen class EmptyScreen() : Screen """ .trimIndent() ) .indented() lint() .files(circuitScreenStub, testFile) .run() .expect( """ src/com/example/screens/EmptyScreen.kt:5: Error: ${CircuitScreenDataClassDetector.MESSAGE} [${CircuitScreenDataClassDetector.ISSUE_ID}] class EmptyScreen() : Screen ~~~~~ 1 error """ .trimIndent() ) } @Test fun `Test CircuitScreenDataClassDetector - local class implements Screen - succeeds`() { val testFile = kotlin( """ package com.example.screens import com.slack.circuit.runtime.screen.Screen fun createScreen() { class LocalScreen : Screen return LocalScreen() } """ .trimIndent() ) .indented() lint().files(circuitScreenStub, testFile).run().expectClean() } private val circuitScreenStub = kotlin( """ package com.slack.circuit.runtime.screen import android.os.Parcelable interface Screen : Parcelable """ .trimIndent() ) .indented() } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/DaggerIssuesDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test class DaggerIssuesDetectorTest : BaseSlackLintTest() { private companion object { private val javaxInjectStubs = kotlin( """ package javax.inject annotation class Inject annotation class Qualifier """ ) .indented() private val daggerStubs = kotlin( """ package dagger annotation class Binds annotation class Provides annotation class Module """ ) .indented() } override val skipTestModes: Array = arrayOf( TestMode.WHITESPACE, TestMode.SUPPRESSIBLE, // This mode adds new parameters to binds() functions, resulting in different error messages TestMode.JVM_OVERLOADS, ) override fun getDetector() = DaggerIssuesDetector() override fun getIssues() = DaggerIssuesDetector.ISSUES.toList() @Test fun `bindings cannot be extension functions`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import javax.inject.Qualifier import dagger.Binds import dagger.Provides import dagger.Module @Qualifier annotation class MyQualifier @Module interface MyModule { @Binds fun Int.bind(): Number @Binds fun Long.bind(): Number @Binds fun Double.bind(): Number @Binds fun Float.bind(): Number @Binds fun Short.bind(): Number @Binds fun Byte.bind(): Number @Binds fun Char.bind(): Comparable @Binds fun String.bind(): Comparable @Binds fun @receiver:MyQualifier Boolean.bind(): Comparable } @Module interface MyModule2 { @Provides fun Int.bind(): Number = this@bind @Provides fun Long.bind(): Number = this@bind @Provides fun Double.bind(): Number = this@bind @Provides fun Float.bind(): Number = this@bind @Provides fun Short.bind(): Number = this@bind @Provides fun Byte.bind(): Number = this@bind @Provides fun Char.bind(): Comparable = this@bind @Provides fun String.bind(): Comparable = this@bind @Provides fun @receiver:MyQualifier Boolean.bind(): Comparable = this@bind } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyQualifier.kt:12: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Int.bind(): Number ~~~ src/foo/MyQualifier.kt:13: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Long.bind(): Number ~~~~ src/foo/MyQualifier.kt:14: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Double.bind(): Number ~~~~~~ src/foo/MyQualifier.kt:15: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Float.bind(): Number ~~~~~ src/foo/MyQualifier.kt:16: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Short.bind(): Number ~~~~~ src/foo/MyQualifier.kt:17: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Byte.bind(): Number ~~~~ src/foo/MyQualifier.kt:18: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun Char.bind(): Comparable ~~~~ src/foo/MyQualifier.kt:19: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun String.bind(): Comparable ~~~~~~ src/foo/MyQualifier.kt:20: Error: @Binds/@Provides functions cannot be extensions [BindingReceiverParameter] @Binds fun @receiver:MyQualifier Boolean.bind(): Comparable ~~~~~~~ src/foo/MyQualifier.kt:25: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Int.bind(): Number = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:26: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Long.bind(): Number = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:27: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Double.bind(): Number = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:28: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Float.bind(): Number = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:29: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Short.bind(): Number = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:30: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Byte.bind(): Number = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:31: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun Char.bind(): Comparable = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:32: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun String.bind(): Comparable = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:33: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun @receiver:MyQualifier Boolean.bind(): Comparable = this@bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 errors, 0 warnings """ .trimIndent() ) } @Test fun `binds type mismatches`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( "src/foo/TestModule.kt", """ package foo import javax.inject.Qualifier import dagger.Binds import dagger.Module sealed interface ItemDetail { object DetailTypeA : ItemDetail } interface ItemMapper class DetailTypeAItemMapper : ItemMapper @Module interface MyModule { @Binds fun validBind(real: Int): Number @Binds fun validBind(real: Boolean): Comparable @Binds fun invalidBind(real: Long): String @Binds fun invalidBind(real: Long): Comparable @Binds fun validComplexBinding(real: DetailTypeAItemMapper): ItemMapper @Binds fun validComplexBinding2(real: DetailTypeAItemMapper): ItemMapper<*> @Binds fun invalidComplexBinding(real: DetailTypeAItemMapper): ItemMapper } """, ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/TestModule.kt:18: Error: @Binds parameter/return must be type-assignable [BindsTypeMismatch] @Binds fun invalidBind(real: Long): String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/TestModule.kt:19: Error: @Binds parameter/return must be type-assignable [BindsTypeMismatch] @Binds fun invalidBind(real: Long): Comparable ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/TestModule.kt:23: Error: @Binds parameter/return must be type-assignable [BindsTypeMismatch] @Binds fun invalidComplexBinding(real: DetailTypeAItemMapper): ItemMapper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } @Test fun `redundant types`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import javax.inject.Qualifier import dagger.Binds import dagger.Module @Qualifier annotation class MyQualifier @Module interface MyModule { @MyQualifier @Binds fun validBind(real: Boolean): Boolean @Binds fun validBind(@MyQualifier real: Boolean): Boolean @Binds fun invalidBind(real: Long): Long @Binds fun invalidBind(real: Long): Long } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyQualifier.kt:13: Error: @Binds functions should return a different type [RedundantBinds] @Binds fun invalidBind(real: Long): Long ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:14: Error: @Binds functions should return a different type [RedundantBinds] @Binds fun invalidBind(real: Long): Long ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `invalid return types`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import javax.inject.Qualifier import dagger.Binds import dagger.Provides import dagger.Module @Qualifier annotation class MyQualifier @Module abstract class MyModule { @Binds fun invalidBind1(@MyQualifier real: Unit) @Binds fun invalidBind2(@MyQualifier real: Unit): Unit @Provides fun invalidBind3() { } @Provides fun invalidBind4(): Unit { return Unit } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyQualifier.kt:14: Error: @Binds/@Provides must have a return type [BindingReturnType] @Provides fun invalidBind3() { ^ src/foo/MyQualifier.kt:17: Error: @Binds/@Provides must have a return type [BindingReturnType] @Provides fun invalidBind4(): Unit { ^ src/foo/MyQualifier.kt:12: Error: @Binds functions must be abstract [BindsMustBeAbstract] @Binds fun invalidBind1(@MyQualifier real: Unit) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyQualifier.kt:13: Error: @Binds functions must be abstract [BindsMustBeAbstract] @Binds fun invalidBind2(@MyQualifier real: Unit): Unit ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun `binds param counts`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import dagger.Binds import dagger.Module @Module interface MyModule { @Binds fun validBind(real: Int): Number @Binds fun invalidBind(real: Int, second: Int): Number @Binds fun invalidBind(): Number } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyModule.kt:8: Error: @Binds must have one parameter [BindsWrongParameterCount] @Binds fun invalidBind(real: Int, second: Int): Number ~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:9: Error: @Binds must have one parameter [BindsWrongParameterCount] @Binds fun invalidBind(): Number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `binds must be abstract`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import dagger.Binds import dagger.Module @Module interface MyModule { @Binds fun validBind(real: Int): Number @Binds fun invalidBind(real: Int): Number { return real } @Binds fun invalidBind(real: Int): Number = real } @Module abstract class MyModule2 { @Binds abstract fun validBind(real: Int): Number @Binds fun invalidBind(real: Int): Number { return real } @Binds fun invalidBind(real: Int): Number = real } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyModule.kt:8: Error: @Binds functions must be abstract [BindsMustBeAbstract] @Binds fun invalidBind(real: Int): Number { return real } ~~~~~~~~~~~~~~~ src/foo/MyModule.kt:9: Error: @Binds functions must be abstract [BindsMustBeAbstract] @Binds fun invalidBind(real: Int): Number = real ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:15: Error: @Binds functions must be abstract [BindsMustBeAbstract] @Binds fun invalidBind(real: Int): Number { return real } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:16: Error: @Binds functions must be abstract [BindsMustBeAbstract] @Binds fun invalidBind(real: Int): Number = real ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun `provides cannot be abstract`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import dagger.Provides import dagger.Module @Module interface MyModule { @Provides fun invalidBind(real: Int): Number @Provides fun invalidBind(real: Int): Number { return real } @Provides fun invalidBind(real: Int): Number = real } @Module abstract class MyModule2 { @Provides abstract fun invalidProvides(real: Int): Number @Provides fun validBind(real: Int): Number { return real } @Provides fun validBind(real: Int): Number = real } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyModule.kt:7: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun invalidBind(real: Int): Number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:8: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun invalidBind(real: Int): Number { return real } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:9: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides fun invalidBind(real: Int): Number = real ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:14: Error: @Provides functions cannot be abstract [ProvidesMustNotBeAbstract] @Provides abstract fun invalidProvides(real: Int): Number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun `must be in a module`() { lint() .files( javaxInjectStubs, daggerStubs, kotlin( """ package foo import dagger.Binds import dagger.Provides import dagger.Module interface MyModule { @Binds fun invalidBind(real: Int): Number companion object { @Provides fun invalidBind(): Int = 3 } } abstract class MyModule2 { @Binds abstract fun invalidBind(real: Int): Number companion object { @Provides fun invalidBind(): Int = 3 } } @Module interface MyModule3 { @Binds fun validBind(real: Int): Number companion object { @Provides fun validBind(): Int = 3 } } @Module abstract class MyModule4 { @Binds abstract fun validBind(real: Int): Number companion object { @Provides fun validBind(): Int = 3 } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyModule.kt:7: Error: @Binds/@Provides functions must be in modules [MustBeInModule] @Binds fun invalidBind(real: Int): Number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:10: Error: @Binds/@Provides functions must be in modules [MustBeInModule] @Provides fun invalidBind(): Int = 3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:15: Error: @Binds/@Provides functions must be in modules [MustBeInModule] @Binds abstract fun invalidBind(real: Int): Number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:18: Error: @Binds/@Provides functions must be in modules [MustBeInModule] @Provides fun invalidBind(): Int = 3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/DeprecatedAnnotationDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Detector import org.junit.Test class DeprecatedAnnotationDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = DeprecatedAnnotationDetector() override fun getIssues() = listOf(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) @Test fun `non-deprecated class has no warnings`() { lint() .files( NON_DEPRECATED_CLASS, java( """ package slack.test; import slack.test.ThisIsNotDeprecated; public class TestClass { public void doStuff() { new ThisIsNotDeprecated(); } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expectClean() } @Test fun `deprecated class has a warning`() { lint() .files( DEPRECATED_CLASS, java( """ package slack.test; import slack.test.ThisIsDeprecated; public class TestClass { public void doStuff() { new ThisIsDeprecated(); } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expect( """ src/slack/test/TestClass.java:8: Warning: This class or method is deprecated; consider using an alternative. [DeprecatedCall] new ThisIsDeprecated(); ~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun `non-deprecated method use no warnings`() { lint() .files( DEPRECATED_METHOD, java( """ package slack.test; import slack.test.ThisIsNotDeprecated; public class TestClass { public void doStuff() { new ThisIsNotDeprecated().thisIsNotDeprecated(); } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expectClean() } @Test fun `deprecated method use has a warning`() { lint() .files( DEPRECATED_METHOD, java( """ package slack.test; import slack.test.ThisIsNotDeprecated; public class TestClass { public void doStuff() { new ThisIsNotDeprecated().thisIsDeprecated(); } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expect( """ src/slack/test/TestClass.java:8: Warning: slack.test.ThisIsNotDeprecated.thisIsDeprecated is deprecated; consider using an alternative. [DeprecatedCall] new ThisIsNotDeprecated().thisIsDeprecated(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun `deprecated method use has a warning in kotlin`() { lint() .files( DEPRECATED_METHOD, kotlin( """ package slack.test import slack.test.ThisIsNotDeprecated class TestClass { public fun doStuff() { ThisIsNotDeprecated().thisIsDeprecated() } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expect( """ src/slack/test/TestClass.kt:8: Warning: slack.test.ThisIsNotDeprecated.thisIsDeprecated is deprecated; consider using an alternative. [DeprecatedCall] ThisIsNotDeprecated().thisIsDeprecated() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun `kotlin-sourced deprecated class use has a warning in kotlin`() { lint() .files( DEPRECATED_CLASS_KOTLIN, kotlin( """ package slack.test import slack.test.ThisIsDeprecated class TestClass { public fun doStuff() { ThisIsDeprecated() } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expect( """ src/slack/test/TestClass.kt:8: Warning: slack.test.ThisIsDeprecated.ThisIsDeprecated is deprecated; consider using an alternative. [DeprecatedCall] ThisIsDeprecated() ~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun `kotlin-sourced deprecated method use has a warning in kotlin`() { lint() .files( DEPRECATED_METHOD_KOTLIN, kotlin( """ package slack.test import slack.test.ThisIsNotDeprecated class TestClass { public fun doStuff() { ThisIsNotDeprecated().thisIsDeprecated() } } """ ) .indented(), ) .issues(DeprecatedAnnotationDetector.ISSUE_DEPRECATED_CALL) .run() .expect( """ src/slack/test/TestClass.kt:8: Warning: slack.test.ThisIsNotDeprecated.thisIsDeprecated is deprecated; consider using an alternative. [DeprecatedCall] ThisIsNotDeprecated().thisIsDeprecated() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } private val DEPRECATED_CLASS = java( """ package slack.test; @Deprecated() class ThisIsDeprecated { } """ ) private val NON_DEPRECATED_CLASS = java( """ package slack.test; import java.lang.Deprecated; class ThisIsNotDeprecated { } """ ) private val DEPRECATED_METHOD = java( """ package slack.test; import java.lang.Deprecated; class ThisIsNotDeprecated { @Deprecated() public void thisIsDeprecated() {} public void thisIsNotDeprecated() {} } """ ) private val DEPRECATED_CLASS_KOTLIN = kotlin( """ package slack.test import kotlin.Deprecated @Deprecated class ThisIsDeprecated { } """ ) private val DEPRECATED_METHOD_KOTLIN = kotlin( """ package slack.test import kotlin.Deprecated class ThisIsNotDeprecated { @Deprecated public fun thisIsDeprecated() { } public fun thisIsNotDeprecated() {} } """ ) } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/DeprecatedSqlUsageDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") class DeprecatedSqlUsageDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = DeprecatedSqlUsageDetector() override fun getIssues(): MutableList = mutableListOf(DeprecatedSqlUsageDetector.ISSUE) @Test fun testJavaInspection() { val deprecatedJavaExample = java( """ package foo; import android.database.sqlite.SQLiteDatabase; public static class SqlUsageTestFailure { public static void delete(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS foo"); } } """ ) .indented() lint() .files(deprecatedJavaExample) .run() .expect( """ src/foo/SqlUsageTestFailure.java:7: Warning: All SQL querying should be performed using SqlDelight [DeprecatedSqlUsage] db.execSQL("DROP TABLE IF EXISTS foo"); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun testKotlinInspection() { val deprecatedKotlinExample = kotlin( """ package foo import android.database.sqlite.SQLiteDatabase object SqlUsageTestFailure { fun delete(db: SQLiteDatabase) { db.execSQL("DROP TABLE IF EXISTS foo") } } """ ) .indented() lint() .files(deprecatedKotlinExample) .run() .expect( """ src/foo/SqlUsageTestFailure.kt:7: Warning: All SQL querying should be performed using SqlDelight [DeprecatedSqlUsage] db.execSQL("DROP TABLE IF EXISTS foo") ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun testInnocentJava() { val innocentJavaExample = java( """ package foo; import android.database.sqlite.SQLiteDatabase; public static class SqlUsageTestFailure { public static void delete(SQLiteDatabase db) { db.getVersion(); } } """ ) .indented() lint().files(innocentJavaExample).run().expectClean() } @Test fun testInnocentKotlin() { val innocentKotlinExample = kotlin( """ package foo object SqlUsageTestFailure { fun delete(db: SupportSQLiteDatabase) { db.getVersion() FeatureFlagStore().update("foo") } } class FeatureFlagStore { fun update(feature: String) = Unit } """ ) .indented() lint().files(innocentKotlinExample).run().expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/DoNotCallProvidersDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 @file:Suppress("UnstableApiUsage") package slack.lint import org.junit.Ignore import org.junit.Test class DoNotCallProvidersDetectorTest : BaseSlackLintTest() { private companion object { private val javaxAnnotation = kotlin( """ package javax.annotation annotation class Generated(val message: String) """ ) .indented() private val daggerStubs = kotlin( """ package dagger annotation class Binds annotation class Provides annotation class Module """ ) .indented() private val daggerProducerStubs = kotlin( """ package dagger.producers annotation class Produces """ ) .indented() } override fun getDetector() = DoNotCallProvidersDetector() override fun getIssues() = listOf(DoNotCallProvidersDetector.ISSUE) @Ignore( "This fails on github actions for some reason, but we're upstreaming this to Dagger anyway" ) @Test fun kotlin() { lint() .files( javaxAnnotation, daggerStubs, daggerProducerStubs, kotlin( """ package foo import dagger.Binds import dagger.Module import dagger.Provides import dagger.producers.Produces import javax.annotation.Generated @Module abstract class MyModule { @Binds fun binds1(input: String): Comparable @Binds fun String.binds2(): Comparable fun badCode() { binds1("this is bad") "this is bad".binds2() provider() producer() } companion object { @Provides fun provider(): String { return "" } @Produces fun producer(): String { return "" } } } @Generated("Totes generated code") abstract class GeneratedCode { fun doStuff() { moduleInstance().binds1("this is technically fine but would never happen in dagger") MyModule.provider() MyModule.producer() } abstract fun moduleInstance(): MyModule } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyModule.kt:15: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] binds1("this is bad") ~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:16: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] "this is bad".binds2() ~~~~~~~~~~~~~~~~~~~~~ src/foo/MyModule.kt:17: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] provider() ~~~~~~~~~~ src/foo/MyModule.kt:18: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] producer() ~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun java() { lint() .files( javaxAnnotation, daggerStubs, daggerProducerStubs, java( """ package foo; import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.producers.Produces; import javax.annotation.Generated; class Holder { @Module abstract class MyModule { @Binds Comparable binds1(String input); void badCode() { binds1("this is bad"); provider(); producer(); } @Provides static String provider() { return ""; } @Produces static String producer() { return ""; } } @Generated("Totes generated code") abstract class GeneratedCode { void doStuff() { moduleInstance().binds1("this is technically fine but would never happen in dagger"); MyModule.provider(); MyModule.producer(); } abstract MyModule moduleInstance(); } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/Holder.java:15: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] binds1("this is bad"); ~~~~~~~~~~~~~~~~~~~~~ src/foo/Holder.java:16: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] provider(); ~~~~~~~~~~ src/foo/Holder.java:17: Error: Dagger provider methods should not be called directly by user code. [DoNotCallProviders] producer(); ~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/ExceptionMessageDetectorTest.kt ================================================ /* * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package slack.lint import org.junit.Test class ExceptionMessageDetectorTest : BaseSlackLintTest() { override fun getDetector() = ExceptionMessageDetector() override fun getIssues() = listOf(ExceptionMessageDetector.ISSUE) @Test fun checkWithMessage() { lint() .files( kotlin( """ package test fun content() { check(true) {"Message"} } """ ) ) .run() .expectClean() } @Test fun checkWithNamedParameterValue() { lint() .files( kotlin( """ package test fun content() { check(value = true) {"Message"} } """ ) ) .run() .expectClean() } @Test fun checkWithNamedParameterMessage() { lint() .files( kotlin( """ package test fun content() { check(true, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun checkWithNamedParameterValueAndMessage() { lint() .files( kotlin( """ package test fun content() { check(value = true, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun checkWithNamedParameterValueAndMessageReversed() { lint() .files( kotlin( """ package test fun content() { check(lazyMessage = {"Message"}, value = true) } """ ) ) .run() .expectClean() } @Test fun checkWithoutMessage() { lint() .files( kotlin( """ package test fun content() { check(true) } """ ) ) .run() .expect( """ src/test/test.kt:5: Error: Please specify a lazyMessage param for check [ExceptionMessage] check(true) ~~~~~ 1 errors, 0 warnings """ ) } @Test fun checkFromDifferentPackageWithoutMessage() { lint() .files( kotlin( """ package test fun content() { check(true) } fun check(boolean: Boolean) {} """ ) ) .run() .expectClean() } @Test fun checkNotNullWithMessage() { lint() .files( kotlin( """ package test fun content() { checkNotNull(null) {"Message"} } """ ) ) .run() .expectClean() } @Test fun checkNotNullWithNamedParameterValue() { lint() .files( kotlin( """ package test fun content() { checkNotNull(value = null) {"Message"} } """ ) ) .run() .expectClean() } @Test fun checkNotNullWithNamedParameterMessage() { lint() .files( kotlin( """ package test fun content() { checkNotNull(null, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun checkNotNullWithNamedParameterValueAndMessage() { lint() .files( kotlin( """ package test fun content() { checkNotNull(value = null, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun checkNotNullWithNamedParameterValueAndMessageReversed() { lint() .files( kotlin( """ package test fun content() { checkNotNull(lazyMessage = {"Message"}, value = null) } """ ) ) .run() .expectClean() } @Test fun checkNotNullWithoutMessage() { lint() .files( kotlin( """ package test fun content() { checkNotNull(null) } """ ) ) .run() .expect( """ src/test/test.kt:5: Error: Please specify a lazyMessage param for checkNotNull [ExceptionMessage] checkNotNull(null) ~~~~~~~~~~~~ 1 errors, 0 warnings """ ) } @Test fun checkNotNullFromDifferentPackageWithoutMessage() { lint() .files( kotlin( """ package test fun content() { checkNotNull(null) } fun checkNotNull(value: Any?) {} """ ) ) .run() .expectClean() } @Test fun requireWithMessage() { lint() .files( kotlin( """ package test fun content() { require(true) {"Message"} } """ ) ) .run() .expectClean() } @Test fun requireWithNamedParameterValue() { lint() .files( kotlin( """ package test fun content() { require(value = true) {"Message"} } """ ) ) .run() .expectClean() } @Test fun requireWithNamedParameterMessage() { lint() .files( kotlin( """ package test fun content() { require(true, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun requireWithNamedParameterValueAndMessage() { lint() .files( kotlin( """ package test fun content() { require(value = true, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun requireWithNamedParameterValueAndMessageReversed() { lint() .files( kotlin( """ package test fun content() { require(lazyMessage = {"Message"}, value = true) } """ ) ) .run() .expectClean() } @Test fun requireWithoutMessage() { lint() .files( kotlin( """ package test fun content() { require(true) } """ ) ) .run() .expect( """ src/test/test.kt:5: Error: Please specify a lazyMessage param for require [ExceptionMessage] require(true) ~~~~~~~ 1 errors, 0 warnings """ ) } @Test fun requireFromDifferentPackageWithoutMessage() { lint() .files( kotlin( """ package test fun content() { require(true) } fun require(boolean: Boolean) {} """ ) ) .run() .expectClean() } @Test fun requireNotNullWithMessage() { lint() .files( kotlin( """ package test fun content() { requireNotNull(null) {"Message"} } """ ) ) .run() .expectClean() } @Test fun requireNotNullWithNamedParameterValue() { lint() .files( kotlin( """ package test fun content() { requireNotNull(value = null) {"Message"} } """ ) ) .run() .expectClean() } @Test fun requireNotNullWithNamedParameterMessage() { lint() .files( kotlin( """ package test fun content() { requireNotNull(null, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun requireNotNullWithNamedParameterValueAndMessage() { lint() .files( kotlin( """ package test fun content() { requireNotNull(value = null, lazyMessage = {"Message"}) } """ ) ) .run() .expectClean() } @Test fun requireNotNullWithNamedParameterValueAndMessageReversed() { lint() .files( kotlin( """ package test fun content() { requireNotNull(lazyMessage = {"Message"}, value = null) } """ ) ) .run() .expectClean() } @Test fun requireNotNullWithoutMessage() { lint() .files( kotlin( """ package test fun content() { requireNotNull(null) } """ ) ) .run() .expect( """ src/test/test.kt:5: Error: Please specify a lazyMessage param for requireNotNull [ExceptionMessage] requireNotNull(null) ~~~~~~~~~~~~~~ 1 errors, 0 warnings """ ) } @Test fun requireNotNullFromDifferentPackageWithoutMessage() { lint() .files( kotlin( """ package test fun content() { requireNotNull(null) } fun requireNotNull(value: Any?) {} """ ) ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/FragmentDaggerFieldInjectionDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 @file:Suppress("UnstableApiUsage") package slack.lint import org.junit.Test class FragmentDaggerFieldInjectionDetectorTest : BaseSlackLintTest() { private val javaxInjectStubs = kotlin( """ package javax.inject annotation class Inject """ ) .indented() private val assistedInjectStubs = kotlin( """ package dagger.assisted annotation class AssistedInject """ ) .indented() private val topLevelFragment = java( """ package androidx.fragment.app; public abstract class Fragment { } """ ) .indented() private val coreUiAbstractFragment = kotlin( """ package slack.coreui.fragment import androidx.fragment.app.Fragment abstract class ViewBindingFragment: Fragment() { } """ ) .indented() override fun getDetector() = FragmentDaggerFieldInjectionDetector() override fun getIssues() = FragmentDaggerFieldInjectionDetector.issues.toList() @Test fun `Kotlin - fragment has field injection warnings`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, kotlin( """ package foo import javax.inject.Inject import slack.coreui.fragment.ViewBindingFragment class MyFragment : ViewBindingFragment() { private lateinit var notAnnotated: String private val defaulted: String = "defaulted" @Inject private lateinit var stringValue1: String @Inject private lateinit var intValue1: Int fun onCreate() { notAnnotated = "fast" } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.kt:11: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ src/foo/MyFragment.kt:13: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `Java - fragment has field injection warnings`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, java( """ package foo; import javax.inject.Inject; import slack.coreui.fragment.ViewBindingFragment; public class MyFragment extends ViewBindingFragment { private static String notAnnotated; private final String defaulted = "defaulted"; @Inject String stringValue1; @Inject Int intValue1; public void onCreate() { notAnnotated = "fast"; } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.java:11: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ src/foo/MyFragment.java:13: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `Kotlin - fragment has field injection errors when constructor injection exists`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, kotlin( """ package foo import javax.inject.Inject import dagger.assisted.AssistedInject import slack.coreui.fragment.ViewBindingFragment class MyFragment @Inject constructor( private val flag: Boolean ): ViewBindingFragment() { private lateinit var notAnnotated: String private val defaulted: String = "defaulted" @Inject private lateinit var stringValue1: String @Inject private lateinit var intValue1: Int fun onCreate() { notAnnotated = "fast" } } class MyFragmentAssistedInject @AssistedInject constructor( private val flag: Boolean ): ViewBindingFragment() { private lateinit var notAnnotated: String private val defaulted: String = "defaulted" @Inject private lateinit var stringValue1: String @Inject private lateinit var intValue1: Int fun onCreate() { notAnnotated = "fast" } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.kt:14: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ src/foo/MyFragment.kt:16: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ src/foo/MyFragment.kt:31: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ src/foo/MyFragment.kt:33: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun `Java - fragment has field injection errors when constructor injection with basic Inject exists`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, java( """ package foo; import javax.inject.Inject; import slack.coreui.fragment.ViewBindingFragment; public class MyFragment extends ViewBindingFragment { private static String notAnnotated; private final String defaulted = "defaulted"; @Inject String stringValue1; @Inject Int intValue1; @Inject public MyFragment(Boolean flag) { } public void onCreate() { notAnnotated = "fast"; } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.java:11: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ src/foo/MyFragment.java:13: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `Java - fragment has field injection errors when constructor injection with AssistedInject exists`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, java( """ package foo; import javax.inject.Inject; import dagger.assisted.AssistedInject; import slack.coreui.fragment.ViewBindingFragment; public class MyFragment extends ViewBindingFragment { private static String notAnnotated; private final String defaulted = "defaulted"; @Inject String stringValue1; @Inject Int intValue1; @AssistedInject public MyFragment(Boolean flag) { } public void onCreate() { notAnnotated = "fast"; } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.java:12: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ src/foo/MyFragment.java:14: Error: Fragment dependencies should be injected using constructor injections only. [FragmentConstructorInjection] @Inject ^ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `Kotlin - abstract fragment does not get warnings`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, kotlin( """ package foo import javax.inject.Inject import androidx.fragment.app.Fragment abstract class MyFragment : Fragment() { private lateinit var notAnnotated: String private val defaulted: String = "defaulted" @Inject private lateinit var stringValue1: String @Inject private lateinit var intValue1: Int fun onCreate() { notAnnotated = "fast" } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.kt:11: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ src/foo/MyFragment.kt:13: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `Kotlin - non-fragment class also get warnings`() { lint() .files( javaxInjectStubs, kotlin( """ package foo import javax.inject.Inject class NonFragment { private lateinit var notAnnotated: String private val defaulted: String = "defaulted" @Inject private lateinit var stringValue1: String @Inject private lateinit var intValue1: Int fun onCreate() { notAnnotated = "fast" } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun `Java - abstract fragment does not get warnings`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, java( """ package foo; import javax.inject.Inject; import dagger.assisted.AssistedInject; import slack.coreui.fragment.ViewBindingFragment; public abstract class MyFragment extends ViewBindingFragment { private static String notAnnotated; private final String defaulted = "defaulted"; @Inject String stringValue1; @Inject Int intValue1; public void onCreate() { notAnnotated = "fast"; } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/MyFragment.java:12: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ src/foo/MyFragment.java:14: Error: Fragment dependencies should be injected using the Fragment's constructor. [FragmentFieldInjection] @Inject ^ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun `Java - non-fragment does not get warnings`() { lint() .files( javaxInjectStubs, assistedInjectStubs, topLevelFragment, coreUiAbstractFragment, java( """ package foo; import javax.inject.Inject; import dagger.assisted.AssistedInject; public class MyFragment { private static String notAnnotated; private final String defaulted = "defaulted"; @Inject String stringValue1; @Inject Int intValue1; public void onCreate() { notAnnotated = "fast"; } } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/GuavaPreconditionsDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestMode import com.android.tools.lint.detector.api.Detector import org.junit.Test class GuavaPreconditionsDetectorTest : BaseSlackLintTest() { override val skipTestModes: Array = arrayOf(TestMode.WHITESPACE) override fun getDetector(): Detector = GuavaPreconditionsDetector() override fun getIssues() = GuavaPreconditionsDetector.issues.toList() private val guavaPreconditionsStub = java( """ package com.google.common.base; public final class Preconditions { public static void checkState(boolean expression) {} public static void checkArgument(boolean expression) {} public static T checkNotNull(T reference) {} public static int checkElementIndex(int index, int size) { return 0; } } """ ) .indented() private val slackPreconditionsStub = kotlin( """ @file:JvmName("JavaPreconditions") package slack.commons fun check(condition: Boolean) {} fun require(condition: Boolean) {} fun checkNotNull(condition: Boolean) {} fun checkNotNull(value: T): T {} """ .trimIndent() ) .indented() @Test fun `Java - Using Guava Preconditions with static reference will show warnings`() { lint() .files( guavaPreconditionsStub, java( """ package foo; import com.google.common.base.Preconditions; public class Foo { boolean isTrue = Preconditions.checkState(1 == 1); void act() { Preconditions.checkState(1 == 1); Preconditions.checkArgument(1 == 1); Preconditions.checkNotNull("Hello"); Preconditions.checkElementIndex(0, 1); } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .run() .expect( """ src/foo/Foo.java:7: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] boolean isTrue = Preconditions.checkState(1 == 1); ~~~~~~~~~~ src/foo/Foo.java:10: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] Preconditions.checkState(1 == 1); ~~~~~~~~~~ src/foo/Foo.java:11: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] Preconditions.checkArgument(1 == 1); ~~~~~~~~~~~~~ src/foo/Foo.java:12: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] Preconditions.checkNotNull("Hello"); ~~~~~~~~~~~~ src/foo/Foo.java:13: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] Preconditions.checkElementIndex(0, 1); ~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/foo/Foo.java line 7: Use Slack's JavaPreconditions checks: @@ -7 +7 - boolean isTrue = Preconditions.checkState(1 == 1); + boolean isTrue = slack.commons.JavaPreconditions.checkState(1 == 1); Fix for src/foo/Foo.java line 10: Use Slack's JavaPreconditions checks: @@ -10 +10 - Preconditions.checkState(1 == 1); + slack.commons.JavaPreconditions.checkState(1 == 1); Fix for src/foo/Foo.java line 11: Use Slack's JavaPreconditions checks: @@ -11 +11 - Preconditions.checkArgument(1 == 1); + slack.commons.JavaPreconditions.checkArgument(1 == 1); Fix for src/foo/Foo.java line 12: Use Slack's JavaPreconditions checks: @@ -12 +12 - Preconditions.checkNotNull("Hello"); + slack.commons.JavaPreconditions.checkNotNull("Hello"); Fix for src/foo/Foo.java line 13: Use Slack's JavaPreconditions checks: @@ -13 +13 - Preconditions.checkElementIndex(0, 1); + slack.commons.JavaPreconditions.checkElementIndex(0, 1); """ .trimIndent() ) } @Test fun `Java - Using Guava Preconditions with fully qualfied references will show warnings`() { lint() .files( guavaPreconditionsStub, java( """ package foo; public class Foo { boolean isTrue = com.google.common.base.Preconditions.checkState(1 == 1); void act() { com.google.common.base.Preconditions.checkState(1 == 1); com.google.common.base.Preconditions.checkArgument(1 == 1); com.google.common.base.Preconditions.checkNotNull("Hello"); com.google.common.base.Preconditions.checkElementIndex(0, 1); } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .run() .expect( """ src/foo/Foo.java:5: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] boolean isTrue = com.google.common.base.Preconditions.checkState(1 == 1); ~~~~~~~~~~ src/foo/Foo.java:8: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] com.google.common.base.Preconditions.checkState(1 == 1); ~~~~~~~~~~ src/foo/Foo.java:9: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] com.google.common.base.Preconditions.checkArgument(1 == 1); ~~~~~~~~~~~~~ src/foo/Foo.java:10: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] com.google.common.base.Preconditions.checkNotNull("Hello"); ~~~~~~~~~~~~ src/foo/Foo.java:11: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] com.google.common.base.Preconditions.checkElementIndex(0, 1); ~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/foo/Foo.java line 5: Use Slack's JavaPreconditions checks: @@ -5 +5 - boolean isTrue = com.google.common.base.Preconditions.checkState(1 == 1); + boolean isTrue = slack.commons.JavaPreconditions.checkState(1 == 1); Fix for src/foo/Foo.java line 8: Use Slack's JavaPreconditions checks: @@ -8 +8 - com.google.common.base.Preconditions.checkState(1 == 1); + slack.commons.JavaPreconditions.checkState(1 == 1); Fix for src/foo/Foo.java line 9: Use Slack's JavaPreconditions checks: @@ -9 +9 - com.google.common.base.Preconditions.checkArgument(1 == 1); + slack.commons.JavaPreconditions.checkArgument(1 == 1); Fix for src/foo/Foo.java line 10: Use Slack's JavaPreconditions checks: @@ -10 +10 - com.google.common.base.Preconditions.checkNotNull("Hello"); + slack.commons.JavaPreconditions.checkNotNull("Hello"); Fix for src/foo/Foo.java line 11: Use Slack's JavaPreconditions checks: @@ -11 +11 - com.google.common.base.Preconditions.checkElementIndex(0, 1); + slack.commons.JavaPreconditions.checkElementIndex(0, 1); """ .trimIndent() ) } @Test fun `Java - Using Guava Preconditions with static imports will show warnings`() { lint() .files( guavaPreconditionsStub, java( """ package foo; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkElementIndex; public class Foo { private boolean isTrue = checkState(1 == 1); void act() { checkState(1 == 1); checkArgument(1 == 1); checkNotNull("Hello"); checkElementIndex(0, 1); } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .run() .expect( """ src/foo/Foo.java:10: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] private boolean isTrue = checkState(1 == 1); ~~~~~~~~~~ src/foo/Foo.java:13: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] checkState(1 == 1); ~~~~~~~~~~ src/foo/Foo.java:14: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] checkArgument(1 == 1); ~~~~~~~~~~~~~ src/foo/Foo.java:15: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] checkNotNull("Hello"); ~~~~~~~~~~~~ src/foo/Foo.java:16: Error: Use Slack's JavaPreconditions instead of Guava's Preconditions checks [GuavaChecksUsed] checkElementIndex(0, 1); ~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/foo/Foo.java line 10: Use Slack's JavaPreconditions checks: @@ -10 +10 - private boolean isTrue = checkState(1 == 1); + private boolean isTrue = slack.commons.JavaPreconditions.checkState(1 == 1); Fix for src/foo/Foo.java line 13: Use Slack's JavaPreconditions checks: @@ -13 +13 - checkState(1 == 1); + slack.commons.JavaPreconditions.checkState(1 == 1); Fix for src/foo/Foo.java line 14: Use Slack's JavaPreconditions checks: @@ -14 +14 - checkArgument(1 == 1); + slack.commons.JavaPreconditions.checkArgument(1 == 1); Fix for src/foo/Foo.java line 15: Use Slack's JavaPreconditions checks: @@ -15 +15 - checkNotNull("Hello"); + slack.commons.JavaPreconditions.checkNotNull("Hello"); Fix for src/foo/Foo.java line 16: Use Slack's JavaPreconditions checks: @@ -16 +16 - checkElementIndex(0, 1); + slack.commons.JavaPreconditions.checkElementIndex(0, 1); """ .trimIndent() ) } @Test fun `Java - Using Slack Preconditions with static reference will be clean`() { lint() .files( slackPreconditionsStub, java( """ package foo; import slack.commons.JavaPreconditions; public class Foo { boolean isTrue = JavaPreconditions.check(1 == 1); void act() { JavaPreconditions.check(1 == 1); JavaPreconditions.require(1 == 1); JavaPreconditions.checkNotNull("Hello"); } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .run() .expectClean() } @Test fun `Java - Using Slack Preconditions with static imports will be clean`() { lint() .files( slackPreconditionsStub, java( """ package foo; import static slack.commons.JavaPreconditions.check; import static slack.commons.JavaPreconditions.checkNotNull; import static slack.commons.JavaPreconditions.require; public class Foo { private boolean isTrue = check(1 == 1); void act() { check(1 == 1); checkNotNull("Hello"); require(true); } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .allowCompilationErrors() // Until AGP 7.1.0 // https://groups.google.com/g/lint-dev/c/BigCO8sMhKU .run() .expectClean() } @Test fun `Kotlin - Using Guava Preconditions with static reference will show warnings`() { lint() .files( guavaPreconditionsStub, kotlin( """ package foo import com.google.common.base.Preconditions class Foo { val isTrue = Preconditions.checkState(false) fun act() { Preconditions.checkState(true) Preconditions.checkArgument(false) Preconditions.checkNotNull("Hello") Preconditions.checkElementIndex(0, 1) } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .run() .expect( """ src/foo/Foo.kt:7: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] val isTrue = Preconditions.checkState(false) ~~~~~~~~~~ src/foo/Foo.kt:10: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] Preconditions.checkState(true) ~~~~~~~~~~ src/foo/Foo.kt:11: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] Preconditions.checkArgument(false) ~~~~~~~~~~~~~ src/foo/Foo.kt:12: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] Preconditions.checkNotNull("Hello") ~~~~~~~~~~~~ src/foo/Foo.kt:13: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] Preconditions.checkElementIndex(0, 1) ~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/foo/Foo.kt line 7: Use Kotlin's standard library checks: @@ -7 +7 - val isTrue = Preconditions.checkState(false) + val isTrue = check(false) Fix for src/foo/Foo.kt line 10: Use Kotlin's standard library checks: @@ -10 +10 - Preconditions.checkState(true) + check(true) Fix for src/foo/Foo.kt line 11: Use Kotlin's standard library checks: @@ -11 +11 - Preconditions.checkArgument(false) + require(false) Fix for src/foo/Foo.kt line 12: Use Kotlin's standard library checks: @@ -12 +12 - Preconditions.checkNotNull("Hello") + checkNotNull("Hello") """ .trimIndent() ) } @Test fun `Kotlin - Using Guava Preconditions with fully qualified references will show warnings`() { lint() .files( guavaPreconditionsStub, kotlin( """ package foo class Foo { val isTrue = com.google.common.base.Preconditions.checkState(false) fun act() { com.google.common.base.Preconditions.checkState(true) com.google.common.base.Preconditions.checkArgument(false) com.google.common.base.Preconditions.checkNotNull("Hello") com.google.common.base.Preconditions.checkElementIndex(0, 1) } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .run() .expect( """ src/foo/Foo.kt:5: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] val isTrue = com.google.common.base.Preconditions.checkState(false) ~~~~~~~~~~ src/foo/Foo.kt:8: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] com.google.common.base.Preconditions.checkState(true) ~~~~~~~~~~ src/foo/Foo.kt:9: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] com.google.common.base.Preconditions.checkArgument(false) ~~~~~~~~~~~~~ src/foo/Foo.kt:10: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] com.google.common.base.Preconditions.checkNotNull("Hello") ~~~~~~~~~~~~ src/foo/Foo.kt:11: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] com.google.common.base.Preconditions.checkElementIndex(0, 1) ~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/foo/Foo.kt line 5: Use Kotlin's standard library checks: @@ -5 +5 - val isTrue = com.google.common.base.Preconditions.checkState(false) + val isTrue = check(false) Fix for src/foo/Foo.kt line 8: Use Kotlin's standard library checks: @@ -8 +8 - com.google.common.base.Preconditions.checkState(true) + check(true) Fix for src/foo/Foo.kt line 9: Use Kotlin's standard library checks: @@ -9 +9 - com.google.common.base.Preconditions.checkArgument(false) + require(false) Fix for src/foo/Foo.kt line 10: Use Kotlin's standard library checks: @@ -10 +10 - com.google.common.base.Preconditions.checkNotNull("Hello") + checkNotNull("Hello") """ .trimIndent() ) } @Test fun `Kotlin - Using Guava Preconditions with static imports will show warnings`() { lint() .files( guavaPreconditionsStub, kotlin( """ package foo import com.google.common.base.Preconditions.checkState import com.google.common.base.Preconditions.checkArgument import com.google.common.base.Preconditions.checkNotNull import com.google.common.base.Preconditions.checkElementIndex class Foo { val isTrue = checkState(false); fun act() { checkState(true) checkArgument(false) checkNotNull("Hello") checkElementIndex(0, 1) } } """ ) .indented(), ) .issues(*GuavaPreconditionsDetector.issues.toTypedArray()) .skipTestModes(TestMode.IMPORT_ALIAS) .run() .expect( """ src/foo/Foo.kt:10: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] val isTrue = checkState(false); ~~~~~~~~~~ src/foo/Foo.kt:13: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] checkState(true) ~~~~~~~~~~ src/foo/Foo.kt:14: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] checkArgument(false) ~~~~~~~~~~~~~ src/foo/Foo.kt:15: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] checkNotNull("Hello") ~~~~~~~~~~~~ src/foo/Foo.kt:16: Error: Kotlin precondition checks should use the Kotlin standard library checks [GuavaPreconditionsUsedInKotlin] checkElementIndex(0, 1) ~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) // Can't assert fix diffs because lint produces non-deterministic diffs per test mode } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/InjectInJavaDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class InjectInJavaDetectorTest : BaseSlackLintTest() { companion object { private val JAVAX_STUBS = kotlin( """ package javax.inject annotation class Inject """ .trimIndent() ) private val DAGGER_STUBS = kotlin( """ package dagger annotation class Module """ .trimIndent() ) private val ASSISTED_STUBS = kotlin( """ package dagger.assisted annotation class AssistedInject annotation class AssistedFactory """ .trimIndent() ) } override fun getDetector() = InjectInJavaDetector() override fun getIssues() = listOf(InjectInJavaDetector.ISSUE) @Test fun kotlinIsOk() { lint() .files( JAVAX_STUBS, DAGGER_STUBS, ASSISTED_STUBS, kotlin( """ package test.pkg import javax.inject.Inject import dagger.Module import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory class KotlinClass @Inject constructor(val constructorInjected: String) { @Inject lateinit var memberInjected: String @Inject fun methodInject(value: String) { } } class KotlinAssistedClass @AssistedInject constructor( @Assisted val assistedParam: String ) { @AssistedFactory interface Factory { fun create(assistedParam: String): KotlinAssistedClass } } @Module object ExampleModule """ ) .indented(), ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun javaIsNotOk() { lint() .files( JAVAX_STUBS, DAGGER_STUBS, ASSISTED_STUBS, java( """ package test.pkg; import javax.inject.Inject; import dagger.Module; import dagger.assisted.AssistedInject; import dagger.assisted.AssistedFactory; class JavaClass { @Inject String memberInjected; @Inject JavaClass(String constructorInjected) { } @Inject void methodInject(String value) { } static class JavaAssistedClass { @AssistedInject JavaAssistedClass(@Assisted String assistedParam) { } @AssistedFactory interface Factory { JavaAssistedClass create(String assistedParam); } } @Module static abstract class ExampleModule { } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/test/pkg/JavaClass.java:9: Error: Only Kotlin classes should be injected in order for Anvil to work. [InjectInJava] @Inject String memberInjected; ~~~~~~~ src/test/pkg/JavaClass.java:11: Error: Only Kotlin classes should be injected in order for Anvil to work. [InjectInJava] @Inject JavaClass(String constructorInjected) { ~~~~~~~ src/test/pkg/JavaClass.java:15: Error: Only Kotlin classes should be injected in order for Anvil to work. [InjectInJava] @Inject void methodInject(String value) { ~~~~~~~ src/test/pkg/JavaClass.java:21: Error: Only Kotlin classes should be injected in order for Anvil to work. [InjectInJava] @AssistedInject JavaAssistedClass(@Assisted String assistedParam) { ~~~~~~~~~~~~~~~ src/test/pkg/JavaClass.java:25: Error: Only Kotlin classes should be injected in order for Anvil to work. [InjectInJava] @AssistedFactory ~~~~~~~~~~~~~~~~ src/test/pkg/JavaClass.java:31: Error: Only Kotlin classes should be injected in order for Anvil to work. [InjectInJava] @Module static abstract class ExampleModule { ~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/JavaOnlyDetectorTest.kt ================================================ // Copyright (C) 2020 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Ignore import org.junit.Test class JavaOnlyDetectorTest : BaseSlackLintTest() { companion object { val ANNOTATIONS: TestFile = kotlin( """ package slack.lint.annotations annotation class KotlinOnly(val reason: String) annotation class JavaOnly(val reason: String) """ ) .indented() } override fun getDetector() = JavaOnlyDetector() override fun getIssues() = listOf(JavaOnlyDetector.ISSUE) // TODO fix these override val skipTestModes: Array = arrayOf(TestMode.SUPPRESSIBLE) @Test fun positive() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/Test.kt", """ package test.pkg import slack.lint.annotations.JavaOnly class Test { @JavaOnly fun g() {} @JavaOnly("satisfying explanation") fun f() {} fun m() { g() f() val r = this::g } } """, ) .indented(), ) .run() .expect( """ test/test/pkg/Test.kt:7: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector] g() ~~~ test/test/pkg/Test.kt:8: Error: This method should not be called from Kotlin: satisfying explanation [JavaOnlyDetector] f() ~~~ test/test/pkg/Test.kt:9: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector] val r = this::g ~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } @Ignore("Un-ignore when https://groups.google.com/forum/#!topic/lint-dev/8Nr0-SDdHbk is fixed") @Test fun positivePackage() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, java( "test/test/pkg/package-info.java", """ @slack.lint.annotations.JavaOnly package test.pkg; import slack.lint.annotations.JavaOnly;""", ) .indented(), java( "test/test/pkg/A.java", """ package test.pkg; public class A { public static void f() {} } """, ) .indented(), kotlin( "test/test/pkg2/Test.kt", """ package test.pkg2 import slack.lint.annotations.JavaOnly class Test { fun m() { test.pkg.A.f() } } """, ) .indented(), ) .run() .expect( """ test/test/pkg2/Test.kt:5: Error: This method should not be called from Kotlin: see its documentation for details. [JavaOnlyDetector] f() ~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun annotateBothFunction() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/Test.kt", """ package test.pkg import slack.lint.annotations.JavaOnly import slack.lint.annotations.KotlinOnly interface Test { @JavaOnly @KotlinOnly fun f() } """, ) .indented(), ) .run() .expect( """ test/test/pkg/Test.kt:5: Error: Cannot annotate functions with both @KotlinOnly and @JavaOnly [JavaOnlyDetector] @JavaOnly @KotlinOnly fun f() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun annotateBothClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/Test.kt", """ package test.pkg import slack.lint.annotations.JavaOnly import slack.lint.annotations.KotlinOnly @JavaOnly @KotlinOnly interface Test { fun f() } """, ) .indented(), ) .run() .expect( """ test/test/pkg/Test.kt:4: Error: Cannot annotate types with both @KotlinOnly and @JavaOnly [JavaOnlyDetector] @JavaOnly @KotlinOnly interface Test { ^ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun requiredOverrideKotlin() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.KotlinOnly interface A { @KotlinOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B : A { override fun f() } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:3: Error: Function overrides f in A which is annotated @KotlinOnly, it should also be annotated. [JavaOnlyDetector] override fun f() ~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for test/test/pkg/B.kt line 3: Add @KotlinOnly: @@ -3 +3 - override fun f() + @slack.lint.annotations.KotlinOnly override fun f() """ .trimIndent() ) } @Test fun annotatedOverrideKotlin() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.KotlinOnly interface A { @KotlinOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.KotlinOnly class B : A { @KotlinOnly override fun f() } """, ) .indented(), ) .run() .expectClean() } @Test fun requiredOverrideJava() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly interface A { @JavaOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B : A { override fun f() } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:3: Error: Function overrides f in A which is annotated @JavaOnly, it should also be annotated. [JavaOnlyDetector] override fun f() ~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for test/test/pkg/B.kt line 3: Add @JavaOnly: @@ -3 +3 - override fun f() + @slack.lint.annotations.JavaOnly override fun f() """ .trimIndent() ) } @Test fun annotatedOverrideJava() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly interface A { @JavaOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.JavaOnly class B : A { @JavaOnly override fun f() } """, ) .indented(), ) .run() .expectClean() } @Test fun requiredOverrideKotlinClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.KotlinOnly @KotlinOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B : A { override fun f() } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:2: Error: Type subclasses/implements A in A.kt which is annotated @KotlinOnly, it should also be annotated. [JavaOnlyDetector] class B : A { ^ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for test/test/pkg/B.kt line 2: Add @KotlinOnly: @@ -2 +2 - class B : A { + @slack.lint.annotations.KotlinOnly class B : A { """ .trimIndent() ) } @Test fun annotatedOverrideKotlinClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.KotlinOnly @KotlinOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.KotlinOnly @KotlinOnly class B : A { override fun f() } """, ) .indented(), ) .run() .expectClean() } @Test fun requiredOverrideJavaClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B : A { override fun f() } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:2: Error: Type subclasses/implements A in A.kt which is annotated @JavaOnly, it should also be annotated. [JavaOnlyDetector] class B : A { ^ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for test/test/pkg/B.kt line 2: Add @JavaOnly: @@ -2 +2 - class B : A { + @slack.lint.annotations.JavaOnly class B : A { """ .trimIndent() ) } @Test fun annotatedOverrideJavaClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly class B : A { override fun f() } """, ) .indented(), ) .run() .expectClean() } // The interface tries to make Object#toString @JavaOnly, and because // the declaration in B is implicit it doesn't get checked. // In practice, making default Object methods @JavaOnly isn't super // useful - typically users interface with the interface directly // (e.g. Hasher) or there's an override that has unwanted behaviour (Localizable). // NOTE: This has slightly different behavior than the error prone checker, as in UAST // the overridden method actually propagates down through types and ErrorProne's doesn't. @Test fun interfaceRedeclaresObjectMethod() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/I.kt", """ package test.pkg import slack.lint.annotations.JavaOnly interface I { @JavaOnly override fun toString(): String } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B : I """, ) .indented(), kotlin( "test/test/pkg/Test.kt", """ package test.pkg class Test { fun f(b: B) { b.toString() val i: I = b i.toString() } } """, ) .indented(), ) .run() .expect( """ test/test/pkg/Test.kt:4: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector] b.toString() ~~~~~~~~~~~~ test/test/pkg/Test.kt:6: Error: This method should not be called from Kotlin, see its documentation for details. [JavaOnlyDetector] i.toString() ~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun lambdaPositiveClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly fun interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B { fun f(): A = {} } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:3: Error: Cannot create lambda instances of @JavaOnly-annotated type A (in A.kt) in Kotlin. Make a concrete class instead. [JavaOnlyDetector] fun f(): A = {} ~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun lambdaPositiveMethod() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly fun interface A { @JavaOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B { fun f(): A = {} } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:3: Error: This method should not be expressed as a lambda in Kotlin, see its documentation for details. [JavaOnlyDetector] fun f(): A = {} ~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun lambdaNegative() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B { fun f(): A = {} } """, ) .indented(), ) .run() .expectClean() } @Test fun lambdaNegativeReturnFun() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly interface A { @JavaOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.JavaOnly class B { @JavaOnly fun f(): A = {} } """, ) .indented(), ) .run() .expectClean() } @Test fun lambdaNegativeReturnClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.JavaOnly class B { @JavaOnly fun f(): A = {} } """, ) .indented(), ) .run() .expectClean() } @Test fun anonymousClassPositiveClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B { fun f(): A = object : A() { override fun f() {} } } """, ) .indented(), ) .run() .expect( """ test/test/pkg/B.kt:3: Error: Cannot create anonymous instances of @JavaOnly-annotated type A (in A.kt) in Kotlin. Make a concrete class instead. [JavaOnlyDetector] fun f(): A = object : A() { ^ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun anonymousClassNegative() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg class B { fun f(): A = object : A() { override fun f() {} } } """, ) .indented(), ) .run() .expectClean() } @Test fun anonymousClassNegativeReturnMethod() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly interface A { @JavaOnly fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.JavaOnly class B { @JavaOnly fun f(): A = object : A() { @JavaOnly override fun f() {} } } """, ) .indented(), ) .run() .expectClean() } @Test fun anonymousClassNegativeReturnClass() { lint() .detector(JavaOnlyDetector()) .files( ANNOTATIONS, kotlin( "test/test/pkg/A.kt", """ package test.pkg import slack.lint.annotations.JavaOnly @JavaOnly interface A { fun f() } """, ) .indented(), kotlin( "test/test/pkg/B.kt", """ package test.pkg import slack.lint.annotations.JavaOnly class B { @JavaOnly fun f(): A = object : A() { override fun f() {} } } """, ) .indented(), ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/JsonInflaterMoshiCompatibilityDetectorTest.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class JsonInflaterMoshiCompatibilityDetectorTest : LintDetectorTest() { override fun getDetector(): Detector { return JsonInflaterMoshiCompatibilityDetector() } override fun getIssues(): List { return listOf(JsonInflaterMoshiCompatibilityDetector.ISSUE) } // Stubs for required annotations and classes private val jsonClassStub = java( """ package com.squareup.moshi; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface JsonClass { boolean generateAdapter(); } """ ) private val adaptedByStub = kotlin( """ package dev.zacsweers.moshix.adapters import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy @Retention(RetentionPolicy.RUNTIME) public annotation class AdaptedBy(val adapter: KClass<*>, val nullSafe: Boolean = true) """ ) private val jsonInflaterStub = kotlin( """ package slack.commons.json import java.lang.reflect.Type interface JsonInflater { fun inflate(jsonData: String, typeOfT: Type): T fun inflate(jsonData: String, clazz: Class): T fun deflate(value: T, clazz: Class): String fun deflate(value: Any, type: Type): String } """ ) private val parameterizedTypeStub = kotlin( """ package com.squareup.moshi import java.lang.reflect.ParameterizedType import java.lang.reflect.Type class StubParameterizedType( private val rawType: Type, private val typeArguments: Array, private val ownerType: Type? = null ) : ParameterizedType { override fun getActualTypeArguments(): Array = typeArguments override fun getRawType(): Type = rawType override fun getOwnerType(): Type? = ownerType } """ .trimIndent() ) private val typeLabelStub = kotlin( """ package dev.zacsweers.moshix.sealed.annotations annotation class TypeLabel(val label: String, val alternateLabels: Array = []) """ ) .indented() private val defaultObjectStub = kotlin( """ package dev.zacsweers.moshix.sealed.annotations annotation class DefaultObject """ ) .indented() @Test fun testDocumentationExample() { testMissingJsonClassAnnotation() } @Test fun testDataClassJsonClassTrue() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater @JsonClass(generateAdapter = true) data class ValidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", ValidModel::class.java) val json = jsonInflater.deflate(model, ValidModel::class.java) } """ ), ) .run() .expectClean() } @Test fun testDataClassJsonClassFalse() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater @JsonClass(generateAdapter = false) data class ValidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", ValidModel::class.java) val json = jsonInflater.deflate(model) } """ ), ) .run() // We only check for the existence of @JsonClass in the detector as we get more granular in // MoshiUsageDetector. .expectClean() } @Test fun testDataClassJsonClassEmpty() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater @JsonClass() data class ValidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", ValidModel::class.java) val json = jsonInflater.deflate(model, ValidModel::class.java) } """ ), ) .run() .expectClean() } @Test fun testDataClassAdaptedBy() { lint() .files( jsonClassStub, jsonInflaterStub, adaptedByStub, kotlin( """ package test import dev.zacsweers.moshix.adapters.AdaptedBy import slack.commons.json.JsonInflater @AdaptedBy(String::class) data class ValidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", ValidModel::class.java) val json = jsonInflater.deflate(model, ValidModel::class.java) } """ ), ) .run() .expectClean() } @Test fun testValidDataClassInList() { lint() .files( jsonClassStub, jsonInflaterStub, adaptedByStub, parameterizedTypeStub, kotlin( """ package test import com.squareup.moshi.JsonClass import com.squareup.moshi.StubParameterizedType import slack.commons.json.JsonInflater @JsonClass(generateAdapter = true) data class ValidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val type = StubParameterizedType( List::class.java, arrayOf(ValidModel::class.java) ) val model = jsonInflater.inflate>("{}", type) val json = jsonInflater.deflate(model, type) } """ ), ) .run() .expectClean() } @Test fun testValidMapOfPrimitives() { lint() .files( jsonClassStub, jsonInflaterStub, adaptedByStub, parameterizedTypeStub, kotlin( """ package test import com.squareup.moshi.JsonClass import com.squareup.moshi.StubParameterizedType import slack.commons.json.JsonInflater fun useJsonInflater(jsonInflater: JsonInflater) { val type = StubParameterizedType( Map::class.java, arrayOf(String::class.java, Int::class.java) ) val model = jsonInflater.inflate>("{}", type) val json = jsonInflater.deflate(model, type) } """ ), ) .run() .expectClean() } @Test fun testInvalidDataClassInList() { lint() .files( jsonClassStub, jsonInflaterStub, adaptedByStub, parameterizedTypeStub, kotlin( """ package test import com.squareup.moshi.JsonClass import com.squareup.moshi.StubParameterizedType import slack.commons.json.JsonInflater data class InvalidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val type = StubParameterizedType( List::class.java, arrayOf(InvalidModel::class.java) ) val model = jsonInflater.inflate>("{}", type) val json = jsonInflater.deflate(model, type) } """ ), ) .run() .expect( """ src/test/InvalidModel.kt:19: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate>("{}", type) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/InvalidModel.kt:20: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, type) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ ) } @Test fun testMissingJsonClassAnnotation() { lint() .files( jsonInflaterStub, kotlin( """ package test import slack.commons.json.JsonInflater data class InvalidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", InvalidModel::class.java) val json = jsonInflater.deflate(model, InvalidModel::class.java) } """ ), ) .run() .expect( """ src/test/InvalidModel.kt:13: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate("{}", InvalidModel::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/InvalidModel.kt:14: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, InvalidModel::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ ) } @Test fun testNonDataClass() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater @JsonClass(generateAdapter = true) class InvalidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", InvalidModel::class.java) val json = jsonInflater.deflate(model, InvalidModel::class.java) } """ ), ) .run() .expectClean() } @Test fun testAbstractClass() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater @JsonClass(generateAdapter = true) abstract class InvalidModel( val id: String, val name: String, val count: Int ) fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", InvalidModel::class.java) val json = jsonInflater.deflate(model, InvalidModel::class.java) } """ ), ) .run() .expect( """ src/test/InvalidModel.kt:15: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate("{}", InvalidModel::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/InvalidModel.kt:16: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, InvalidModel::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ ) } @Test fun testEnumClass() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater @JsonClass(generateAdapter = false) enum class ValidEnum { UNKNOWN, UP, DOWN, LEFT, RIGHT } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", ValidEnum::class.java) val json = jsonInflater.deflate(model, ValidEnum::class.java) } """ ), ) .run() .expectClean() } @Test fun testEnumClassMissingJsonClassAnnotation() { lint() .files( jsonClassStub, jsonInflaterStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater enum class ValidEnum { UNKNOWN, UP, DOWN, LEFT, RIGHT } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", ValidEnum::class.java) val json = jsonInflater.deflate(model, ValidEnum::class.java) } """ ), ) .run() .expect( """ src/test/ValidEnum.kt:16: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate("{}", ValidEnum::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/ValidEnum.kt:17: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, ValidEnum::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors """ ) } @Test fun testNonSealedInterface() { lint() .files( jsonClassStub, jsonInflaterStub, typeLabelStub, defaultObjectStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @JsonClass(generateAdapter = true, generator = "sealed:type") interface Animal { @TypeLabel("dog") @JsonClass(generateAdapter = true) data class Dog(val name: String) : Animal @TypeLabel("cat") @JsonClass(generateAdapter = true) data class Cat(val age: Int) : Animal @DefaultObject object Default : Animal } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", Animal::class.java) val json = jsonInflater.deflate(model, Animal::class.java) } """ ), ) .run() .expect( """ src/test/Animal.kt:24: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate("{}", Animal::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/Animal.kt:25: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, Animal::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors """ ) } @Test fun testSealedInterface() { lint() .files( jsonClassStub, jsonInflaterStub, typeLabelStub, defaultObjectStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @JsonClass(generateAdapter = true, generator = "sealed:type") sealed interface Animal { @TypeLabel("dog") @JsonClass(generateAdapter = true) data class Dog(val name: String) : Animal @TypeLabel("cat") @JsonClass(generateAdapter = true) data class Cat(val age: Int) : Animal @DefaultObject object Default : Animal } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", Animal::class.java) val json = jsonInflater.deflate(model, Animal::class.java) } """ ), ) .run() .expectClean() } @Test fun testSealedInterfaceMissingJsonClassAnnotation() { lint() .files( jsonClassStub, jsonInflaterStub, typeLabelStub, defaultObjectStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject sealed interface Animal { @TypeLabel("dog") @JsonClass(generateAdapter = true) data class Dog(val name: String) : Animal @TypeLabel("cat") @JsonClass(generateAdapter = true) data class Cat(val age: Int) : Animal @DefaultObject object Default : Animal } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", Animal::class.java) val json = jsonInflater.deflate(model, Animal::class.java) } """ ), ) .run() .expect( """ src/test/Animal.kt:23: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate("{}", Animal::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/Animal.kt:24: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, Animal::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors """ ) } @Test fun testSealedClass() { lint() .files( jsonClassStub, jsonInflaterStub, typeLabelStub, defaultObjectStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class Animal { @TypeLabel("dog") @JsonClass(generateAdapter = true) data class Dog(val name: String) : Animal() @TypeLabel("cat") @JsonClass(generateAdapter = true) data class Cat(val age: Int) : Animal() @DefaultObject object Default : Animal() } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", Animal::class.java) val json = jsonInflater.deflate(model, Animal::class.java) } """ ), ) .run() .expectClean() } @Test fun testSealedClassMissingJsonClassAnnotation() { lint() .files( jsonClassStub, jsonInflaterStub, typeLabelStub, defaultObjectStub, kotlin( """ package test import com.squareup.moshi.JsonClass import slack.commons.json.JsonInflater import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject sealed class Animal { @TypeLabel("dog") @JsonClass(generateAdapter = true) data class Dog(val name: String) : Animal() @TypeLabel("cat") @JsonClass(generateAdapter = true) data class Cat(val age: Int) : Animal() @DefaultObject object Default : Animal() } fun useJsonInflater(jsonInflater: JsonInflater) { val model = jsonInflater.inflate("{}", Animal::class.java) val json = jsonInflater.deflate(model, Animal::class.java) } """ ), ) .run() .expect( """ src/test/Animal.kt:23: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val model = jsonInflater.inflate("{}", Animal::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/Animal.kt:24: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType] val json = jsonInflater.deflate(model, Animal::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors """ ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/LintKotlinVersionCheckTest.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import org.jetbrains.uast.UMethod import org.junit.Test import slack.lint.util.sourceImplementation /** * Lint chases Kotlin versions differently than what we declare our build with. This test is just * here to catch those for our own awareness and should be updated whenever lint updates its own. */ class LintKotlinVersionCheckTest : BaseSlackLintTest() { override fun getDetector(): Detector = KotlinVersionDetector() override fun getIssues() = listOf(KotlinVersionDetector.ISSUE) @Test fun check() { lint() .files( kotlin( """ package test fun main() { println("Hello, world!") } """ ) .indented() ) .run() .expect( """ src/test/test.kt:3: Error: Kotlin version matched expected one [KotlinVersion] fun main() { ^ 1 errors, 0 warnings """ .trimIndent() ) } class KotlinVersionDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UMethod::class.java) override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitMethod(node: UMethod) { if (KotlinVersion.CURRENT == EXPECTED_VERSION) { // Report something anyway to ensure our lint was correctly picked up at least context.report( ISSUE, node, context.getLocation(node), "Kotlin version matched expected one", ) } else { context.report( ISSUE, node, context.getLocation(node), "Kotlin version was ${KotlinVersion.CURRENT}, expected $EXPECTED_VERSION", ) } } } } companion object { private val EXPECTED_VERSION = TestBuildConfig.LINT_KOTLIN_VERSION val ISSUE = Issue.create( "KotlinVersion", "Kotlin version", "Kotlin version", sourceImplementation(), severity = Severity.ERROR, ) } } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/MainScopeUsageDetectorTest.kt ================================================ // Copyright (C) 2020 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test class MainScopeUsageDetectorTest : BaseSlackLintTest() { companion object { private val COROUTINE_SCOPE_STUB = kotlin( "test/kotlinx/coroutines/CoroutineScope.kt", // language=kotlin """ package kotlinx.coroutines fun MainScope() { } """ .trimIndent(), ) } override val skipTestModes: Array = arrayOf(TestMode.WHITESPACE) override fun getDetector() = MainScopeUsageDetector() override fun getIssues() = listOf(MainScopeUsageDetector.ISSUE) @Test fun simple() { lint() .files( COROUTINE_SCOPE_STUB, kotlin( """ package test.pkg import kotlinx.coroutines.MainScope fun example() { val scope = MainScope() } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .skipTestModes(TestMode.IMPORT_ALIAS) .run() .expect( """ src/test/pkg/test.kt:6: Error: Use slack.foundation.coroutines.android.MainScope. [MainScopeUsage] val scope = MainScope() ~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/test/pkg/test.kt line 6: Use slack.foundation.coroutines.android.MainScope: @@ -6 +6 - val scope = MainScope() + val scope = slack.foundation.coroutines.android.MainScope() """ .trimIndent() ) } // Usage in tests are fine @Test fun testsAreFine() { lint() .files( COROUTINE_SCOPE_STUB, kotlin( "test/test/pkg/Test.kt", """ package test.pkg import kotlinx.coroutines.MainScope fun example() { val scope = MainScope() } """ .trimIndent(), ) .indented(), ) .allowCompilationErrors(false) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/MoshiEnumUsageDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class MoshiEnumUsageDetectorTest : BaseSlackLintTest() { private val jsonClassAnnotation = java( """ package com.squareup.moshi; public @interface JsonClass { boolean generateAdapter(); String generator() default ""; } """ ) .indented() private val jsonAnnotation = java( """ package com.squareup.moshi; public @interface Json { String name(); } """ ) .indented() override fun getDetector() = MoshiUsageDetector() override fun getIssues() = MoshiUsageDetector.issues().toList() @Test fun java_correct() { val correctJava = java( """ package slack.model; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) enum TestEnum { UNKNOWN, TEST } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, correctJava).run().expectClean() } @Test fun java_expected_unknown_is_ok() { val correctJava = java( """ package slack.model; import com.squareup.moshi.JsonClass; import com.squareup.moshi.Json; @JsonClass(generateAdapter = false) enum TestEnum { UNKNOWN, TEST, @Json(name = "UNKNOWN") EXPECTED_UNKNOWN } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, correctJava).run().expectClean() } @Test fun java_ignored() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; public enum TestEnum { TEST } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, source).run().expectClean() } @Test fun java_generateAdapter_isTrue() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = true) public enum TestEnum { UNKNOWN, TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:5: Error: Enums annotated with @JsonClass must not set generateAdapter to true. [MoshiUsageEnumJsonClassGenerated] @JsonClass(generateAdapter = true) ~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/TestEnum.java line 5: Set to false: @@ -5 +5 - @JsonClass(generateAdapter = true) + @JsonClass(generateAdapter = false) """ .trimIndent() ) } @Test fun java_custom_generator_is_ignored() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = true, generator = "custom") public enum TestEnum { TEST } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, source).run().expectClean() } @Test fun java_unknown_annotated() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; import com.squareup.moshi.Json; @JsonClass(generateAdapter = false) public enum TestEnum { @Json(name = "unknown") UNKNOWN, TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:7: Error: UNKNOWN members in @JsonClass-annotated enums should not be annotated with @Json [MoshiUsageEnumAnnotatedUnknown] public enum TestEnum { ~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun java_unknown_wrong_order() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) public enum TestEnum { TEST, UNKNOWN } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:8: Error: Enums serialized with Moshi must reserve the first member as UNKNOWN. [MoshiUsageEnumMissingUnknown] UNKNOWN ~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun java_unknown_wrong_order_inferred_enum() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; import com.squareup.moshi.Json; @JsonClass(generateAdapter = false) public enum TestEnum { @Json(name = "test") TEST, UNKNOWN } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:10: Error: Enums serialized with Moshi must reserve the first member as UNKNOWN. [MoshiUsageEnumMissingUnknown] UNKNOWN ~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun java_json_but_missing_json_class() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; import com.squareup.moshi.Json; public enum TestEnum { UNKNOWN, @Json(name = "test") TEST; } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:6: Error: Enums serialized with Moshi should be annotated with @JsonClass. [MoshiUsageEnumMissingJsonClass] public enum TestEnum { ~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun java_lower_casing() { val source = java( """ package slack.model; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) enum TestEnum { UNKNOWN, test } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:8: Warning: Consider using @Json(name = ...) rather than lower casing. [MoshiUsageEnumCasing] test ~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun java_lower_casing_already_annotated() { val source = java( """ package slack.model; import com.squareup.moshi.Json; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) enum TestEnum { UNKNOWN, @Json(name = "taken") test } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:9: Warning: Consider using @Json(name = ...) rather than lower casing. [MoshiUsageEnumCasing] @Json(name = "taken") test ~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/TestEnum.java line 9: Rename to 'TEST': @@ -9 +9 - @Json(name = "taken") test + @Json(name = "taken") TEST """ .trimIndent() ) } @Test fun java_json_name_blank() { val source = java( """ package slack.model; import com.squareup.moshi.Json; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) enum TestEnum { UNKNOWN, @Json(name = " ") TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:9: Error: Don't use blank names in @Json. [MoshiUsageBlankJsonName] @Json(name = " ") TEST ~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun java_json_name_empty() { val source = java( """ package slack.model; import com.squareup.moshi.Json; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) enum TestEnum { UNKNOWN, @Json(name = "") TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.java:9: Error: Don't use blank names in @Json. [MoshiUsageBlankJsonName] @Json(name = "") TEST ~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun java_redundantJsonName() { val source = java( """ package slack.model; import com.squareup.moshi.Json; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) public enum Example { UNKNOWN, @Json(name = "VALUE") VALUE } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/Example.java:9: Warning: Json.name with the same value as the property/enum member name is redundant. [MoshiUsageRedundantJsonName] @Json(name = "VALUE") VALUE ~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/Example.java line 9: Remove '@Json(name = "VALUE")': @@ -9 +9 - @Json(name = "VALUE") VALUE + VALUE """ .trimIndent() ) } @Test fun java_duplicateJsonNames() { val source = java( """ package slack.model; import com.squareup.moshi.Json; import com.squareup.moshi.JsonClass; @JsonClass(generateAdapter = false) public enum Example { UNKNOWN, VALUE, @Json(name = "VALUE") VALUE_1, @Json(name = "value2") VALUE_2, @Json(name = "value2") VALUE_3; } """ ) .indented() lint() .files(jsonAnnotation, jsonClassAnnotation, source) .run() .expect( """ src/slack/model/Example.java:9: Error: Name 'VALUE' is duplicated by member 'VALUE_1'. [MoshiUsageDuplicateJsonName] VALUE, ~~~~~ src/slack/model/Example.java:10: Error: Name 'VALUE' is duplicated by member 'VALUE'. [MoshiUsageDuplicateJsonName] @Json(name = "VALUE") VALUE_1, ~~~~~~~ src/slack/model/Example.java:11: Error: Name 'value2' is duplicated by member 'VALUE_3'. [MoshiUsageDuplicateJsonName] @Json(name = "value2") VALUE_2, ~~~~~~~ src/slack/model/Example.java:12: Error: Name 'value2' is duplicated by member 'VALUE_2'. [MoshiUsageDuplicateJsonName] @Json(name = "value2") VALUE_3; ~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_correct() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, TEST } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, source).run().expectClean() } @Test fun kotlin_expected_unknown_is_ok() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, TEST, @Json(name = "UNKNOWN") EXPECTED_UNKNOWN } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, source).run().expectClean() } @Test fun kotlin_ignored() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass enum class TestEnum { TEST } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, source).run().expectClean() } @Test fun kotlin_generateAdapter_isTrue() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) enum class TestEnum { UNKNOWN, TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:5: Error: Enums annotated with @JsonClass must not set generateAdapter to true. [MoshiUsageEnumJsonClassGenerated] @JsonClass(generateAdapter = true) ~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/TestEnum.kt line 5: Set to false: @@ -5 +5 - @JsonClass(generateAdapter = true) + @JsonClass(generateAdapter = false) """ .trimIndent() ) } @Test fun kotlin_custom_generator_is_ignored() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = "custom") enum class TestEnum { TEST } """ ) .indented() lint().files(jsonClassAnnotation, jsonAnnotation, source).run().expectClean() } @Test fun kotlin_unknown_annotated() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = false) enum class TestEnum { @Json(name = "unknown") UNKNOWN, TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:7: Error: UNKNOWN members in @JsonClass-annotated enums should not be annotated with @Json [MoshiUsageEnumAnnotatedUnknown] enum class TestEnum { ~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_unknown_wrong_order() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class TestEnum { TEST, UNKNOWN } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:8: Error: Enums serialized with Moshi must reserve the first member as UNKNOWN. [MoshiUsageEnumMissingUnknown] UNKNOWN ~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_unknown_wrong_order_inferred_enum() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = false) enum class TestEnum { @Json(name = "test") TEST, UNKNOWN } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:10: Error: Enums serialized with Moshi must reserve the first member as UNKNOWN. [MoshiUsageEnumMissingUnknown] UNKNOWN ~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_json_but_missing_json_class() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json enum class TestEnum { UNKNOWN, @Json(name = "test") TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:6: Error: Enums serialized with Moshi should be annotated with @JsonClass. [MoshiUsageEnumMissingJsonClass] enum class TestEnum { ~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_lower_casing() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, test } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:8: Warning: Consider using @Json(name = ...) rather than lower casing. [MoshiUsageEnumCasing] test ~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/TestEnum.kt line 8: Add @Json(name = "test") and rename to 'TEST': @@ -8 +8 - test + @com.squareup.moshi.Json(name = "test") TEST """ .trimIndent() ) } @Test fun kotlin_lower_casing_already_annotated() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, @Json(name = "taken") test } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:9: Warning: Consider using @Json(name = ...) rather than lower casing. [MoshiUsageEnumCasing] @Json(name = "taken") test ~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/TestEnum.kt line 9: Rename to 'TEST': @@ -9 +9 - @Json(name = "taken") test + @Json(name = "taken") TEST """ .trimIndent() ) } @Test fun kotlin_json_name_blank() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, @Json(name = " ") TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:9: Error: Don't use blank names in @Json. [MoshiUsageBlankJsonName] @Json(name = " ") TEST ~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_json_name_empty() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, @Json(name = "") TEST } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/TestEnum.kt:9: Error: Don't use blank names in @Json. [MoshiUsageBlankJsonName] @Json(name = "") TEST ~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_redundantJsonName() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class Example { UNKNOWN, @Json(name = "VALUE") VALUE } """ ) .indented() lint() .files(jsonClassAnnotation, jsonAnnotation, source) .run() .expect( """ src/slack/model/Example.kt:9: Warning: Json.name with the same value as the property/enum member name is redundant. [MoshiUsageRedundantJsonName] @Json(name = "VALUE") VALUE ~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/model/Example.kt line 9: Remove '@Json(name = "VALUE")': @@ -9 +9 - @Json(name = "VALUE") VALUE + VALUE """ .trimIndent() ) } @Test fun kotlin_duplicateJsonNames() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class Example { UNKNOWN, VALUE, @Json(name = "VALUE") VALUE_1, @Json(name = "value2") VALUE_2, @Json(name = "value2") VALUE_3 } """ ) .indented() lint() .files(jsonAnnotation, jsonClassAnnotation, source) .run() .expect( """ src/slack/model/Example.kt:9: Error: Name 'VALUE' is duplicated by member 'VALUE_1'. [MoshiUsageDuplicateJsonName] VALUE, ~~~~~ src/slack/model/Example.kt:10: Error: Name 'VALUE' is duplicated by member 'VALUE'. [MoshiUsageDuplicateJsonName] @Json(name = "VALUE") VALUE_1, ~~~~~~~ src/slack/model/Example.kt:11: Error: Name 'value2' is duplicated by member 'VALUE_3'. [MoshiUsageDuplicateJsonName] @Json(name = "value2") VALUE_2, ~~~~~~~ src/slack/model/Example.kt:12: Error: Name 'value2' is duplicated by member 'VALUE_2'. [MoshiUsageDuplicateJsonName] @Json(name = "value2") VALUE_3 ~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/MoshiUsageDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test class MoshiUsageDetectorTest : BaseSlackLintTest() { private val keepAnnotation = java( """ package androidx.annotation; public @interface Keep { } """ ) .indented() private val jsonClassAnnotation = java( """ package com.squareup.moshi; public @interface JsonClass { boolean generateAdapter(); String generator() default ""; } """ ) .indented() private val jsonAnnotation = java( """ package com.squareup.moshi; public @interface Json { String name(); } """ ) .indented() private val jsonQualifierAnnotation = java( """ package com.squareup.moshi; public @interface JsonQualifier { } """ ) .indented() private val typeLabel = kotlin( """ package dev.zacsweers.moshix.sealed.annotations annotation class TypeLabel(val label: String, val alternateLabels: Array = []) """ ) .indented() private val defaultObject = kotlin( """ package dev.zacsweers.moshix.sealed.annotations annotation class DefaultObject """ ) .indented() private val adaptedBy = kotlin( """ package dev.zacsweers.moshix.adapters import kotlin.reflect.KClass annotation class AdaptedBy( val adapter: KClass<*>, val nullSafe: Boolean = true ) """ ) .indented() private val jsonAdapter = java( """ package com.squareup.moshi; public class JsonAdapter { public interface Factory { } } """ ) .indented() override val skipTestModes: Array = arrayOf( TestMode.WHITESPACE, TestMode.SUPPRESSIBLE, // Aliases are impossible to test correctly because you have to maintain completely different // expected fixes and source inputs TestMode.TYPE_ALIAS, TestMode.IMPORT_ALIAS, ) override fun getDetector() = MoshiUsageDetector() override fun getIssues() = MoshiUsageDetector.issues().toList() @Test fun simpleCorrect() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class TestClass(@Json(name = "bar") val foo: String) """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun sealed_correct() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class BaseType { @TypeLabel(label = "nested") @JsonClass(generateAdapter = true) data class Nested(val foo: String) : BaseType() } @TypeLabel(label = "one") @JsonClass(generateAdapter = true) data class Subtype(val foo: String) : BaseType() @TypeLabel(label = "two") object ObjectSubType : BaseType() @DefaultObject object Default : BaseType() // Cover for making sure listing interfaces before superclasses don't affect // superclass lookups @TypeLabel(label = "three") @JsonClass(generateAdapter = true) data class SubtypeWithInterface(val foo: String) : ARandomInterface, BaseType() interface ARandomInterface """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun sealed_interface_correct() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @JsonClass(generateAdapter = true, generator = "sealed:type") sealed interface BaseType { @TypeLabel(label = "nested") @JsonClass(generateAdapter = true) data class Nested(val foo: String) : BaseType } @TypeLabel(label = "one") @JsonClass(generateAdapter = true) data class Subtype(val foo: String) : BaseType @TypeLabel(label = "two") object ObjectSubType : BaseType @DefaultObject object Default : BaseType // Cover for making sure listing interfaces before superclasses don't affect // superclass lookups @TypeLabel(label = "three") @JsonClass(generateAdapter = true) data class SubtypeWithInterface(val foo: String) : ARandomInterface, BaseType interface ARandomInterface """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun sealed_generic() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.sealed.annotations.TypeLabel @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class BaseType @TypeLabel(label = "one") @JsonClass(generateAdapter = true) data class Subtype(val foo: T) : BaseType() // This is "ok" generics use because the subtype itself has none @TypeLabel(label = "two") @JsonClass(generateAdapter = true) data class SubtypeTwo(val foo: String) : BaseType() """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/BaseType.kt:11: Error: Sealed subtypes used with moshi-sealed cannot be generic. [MoshiUsageGenericSealedSubtype] data class Subtype(val foo: T) : BaseType() ~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun sealed_missing_base_type() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @TypeLabel(label = "one") @JsonClass(generateAdapter = true) data class Subtype(val foo: String) @TypeLabel(label = "two") object ObjectSubType @DefaultObject object Default """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Subtype.kt:7: Error: Inappropriate @TypeLabel or @DefaultObject annotation. [MoshiUsageInappropriateTypeLabel] @TypeLabel(label = "one") ~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Subtype.kt:11: Error: Inappropriate @TypeLabel or @DefaultObject annotation. [MoshiUsageInappropriateTypeLabel] @TypeLabel(label = "two") ~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Subtype.kt:14: Error: Inappropriate @TypeLabel or @DefaultObject annotation. [MoshiUsageInappropriateTypeLabel] @DefaultObject ~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Subtype.kt line 7: Remove '@TypeLabel(label = "one")': @@ -7 +7 - @TypeLabel(label = "one") Autofix for src/slack/model/Subtype.kt line 11: Remove '@TypeLabel(label = "two")': @@ -11 +11 - @TypeLabel(label = "two") Autofix for src/slack/model/Subtype.kt line 14: Remove '@DefaultObject': @@ -14 +14 - @DefaultObject """ .trimIndent() ) } @Test fun sealed_double_annotation() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.sealed.annotations.TypeLabel import dev.zacsweers.moshix.sealed.annotations.DefaultObject @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class BaseType @TypeLabel(label = "one") @DefaultObject object Default : BaseType() """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/BaseType.kt:10: Error: Only use one of @TypeLabel or @DefaultObject. [MoshiUsageDoubleTypeLabel] @TypeLabel(label = "one") ~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/BaseType.kt:11: Error: Only use one of @TypeLabel or @DefaultObject. [MoshiUsageDoubleTypeLabel] @DefaultObject ~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/BaseType.kt line 10: Remove '@TypeLabel(label = "one")': @@ -10 +10 - @TypeLabel(label = "one") Autofix for src/slack/model/BaseType.kt line 11: Remove '@DefaultObject': @@ -11 +11 - @DefaultObject """ .trimIndent() ) } @Test fun sealed_missing_type_label() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class BaseType @JsonClass(generateAdapter = true) data class Subtype(val foo: String) : BaseType() object ObjectSubType : BaseType() """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/BaseType.kt:9: Error: Sealed Moshi subtypes must be annotated with @TypeLabel or @DefaultObject. [MoshiUsageMissingTypeLabel] data class Subtype(val foo: String) : BaseType() ~~~~~~~ src/slack/model/BaseType.kt:11: Error: Sealed Moshi subtypes must be annotated with @TypeLabel or @DefaultObject. [MoshiUsageMissingTypeLabel] object ObjectSubType : BaseType() ~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun sealed_must_be_sealed() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = "sealed:type") abstract class BaseType """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/BaseType.kt:6: Error: Moshi-sealed can only be applied to 'sealed' types. [MoshiUsageSealedMustBeSealed] abstract class BaseType ~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun sealed_blank_type() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = "sealed:") sealed class BaseType """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/BaseType.kt:5: Error: Moshi-sealed requires a type label specified after the 'sealed:' prefix. [MoshiUsageBlankTypeLabel] @JsonClass(generateAdapter = true, generator = "sealed:") ~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun empty_generator() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = "") data class Example(val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:5: Error: Don't use blank JsonClass.generator values. [MoshiUsageBlankGenerator] @JsonClass(generateAdapter = true, generator = "") ~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun blank_generator() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = " ") data class Example(val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:5: Error: Don't use blank JsonClass.generator values. [MoshiUsageBlankGenerator] @JsonClass(generateAdapter = true, generator = " ") ~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun private_constructor() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example private constructor(val value: String) @JsonClass(generateAdapter = true) data class Example2 protected constructor(val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Error: Constructors in Moshi classes cannot be private. [MoshiUsagePrivateConstructor] data class Example private constructor(val value: String) ~~~~~~~ src/slack/model/Example.kt:9: Error: Constructors in Moshi classes cannot be private. [MoshiUsagePrivateConstructor] data class Example2 protected constructor(val value: String) ~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 6: Make constructor 'internal': @@ -6 +6 - data class Example private constructor(val value: String) + data class Example internal constructor(val value: String) Autofix for src/slack/model/Example.kt line 9: Make constructor 'internal': @@ -9 +9 - data class Example2 protected constructor(val value: String) @@ -10 +9 + data class Example2 internal constructor(val value: String) """ .trimIndent() ) } @Test fun params_that_need_init() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import kotlin.jvm.Transient @JsonClass(generateAdapter = true) class Example(val value: String, nonProp: String, @Transient val transientProp: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:7: Error: Constructor non-property parameters in Moshi classes must have default values. [MoshiUsageParamNeedsInit] class Example(val value: String, nonProp: String, @Transient val transientProp: String) ~~~~~~~~~~~~~~~ src/slack/model/Example.kt:7: Error: Transient constructor properties must have default values. [MoshiUsageTransientNeedsInit] class Example(val value: String, nonProp: String, @Transient val transientProp: String) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:7: Error: Model classes should be immutable data classes. [MoshiUsageUseData] class Example(val value: String, nonProp: String, @Transient val transientProp: String) ~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } @Test fun private_prop() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example(private val value: String, protected val value2: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Error: Constructor parameter properties in Moshi classes cannot be private. [MoshiUsagePrivateConstructorProperty] data class Example(private val value: String, protected val value2: String) ~~~~~~~ src/slack/model/Example.kt:6: Error: Constructor parameter properties in Moshi classes cannot be private. [MoshiUsagePrivateConstructorProperty] data class Example(private val value: String, protected val value2: String) ~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 6: Make value 'internal': @@ -6 +6 - data class Example(private val value: String, protected val value2: String) @@ -7 +6 + data class Example(internal val value: String, protected val value2: String) Autofix for src/slack/model/Example.kt line 6: Make value2 'internal': @@ -6 +6 - data class Example(private val value: String, protected val value2: String) @@ -7 +6 + data class Example(private val value: String, internal val value2: String) """ .trimIndent() ) } @Test fun mutable_prop() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example(var value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Warning: Moshi properties should be immutable. [MoshiUsageVarProperty] data class Example(var value: String) ~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 6: Make value 'val': @@ -6 +6 - data class Example(var value: String) @@ -7 +6 + data class Example(val value: String) """ .trimIndent() ) } @Test fun generateAdapter_should_be_true() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) data class Example(val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:5: Error: JsonClass.generateAdapter must be true in order for Moshi code gen to run. [MoshiUsageGenerateAdapterShouldBeTrue] @JsonClass(generateAdapter = false) ~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun empty_json_name() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class Example(@Json(name = "") val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:7: Error: Don't use blank names in @Json. [MoshiUsageBlankJsonName] data class Example(@Json(name = "") val value: String) ~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun blank_json_name() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class Example(@Json(name = " ") val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:7: Error: Don't use blank names in @Json. [MoshiUsageBlankJsonName] data class Example(@Json(name = " ") val value: String) ~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun jsonNameSiteTargets() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class Example( @field:Json(name = "foo") val value: String ) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:8: Error: Use of site-targets on @Json are redundant. [MoshiUsageRedundantSiteTarget] @field:Json(name = "foo") val value: String ~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 8: Remove 'field:': @@ -8 +8 - @field:Json(name = "foo") val value: String + @Json(name = "foo") val value: String """ .trimIndent() ) } @Test fun jsonNameMultiple() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class Example( @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:8: Error: Use of site-targets on @Json are redundant. [MoshiUsageRedundantSiteTarget] @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:8: Error: Use of site-targets on @Json are redundant. [MoshiUsageRedundantSiteTarget] @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:8: Error: Use of site-targets on @Json are redundant. [MoshiUsageRedundantSiteTarget] @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ~~~~~~~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 8: Remove '@field:Json(name = "foo")': @@ -8 +8 - @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String + @Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String Autofix for src/slack/model/Example.kt line 8: Remove '@property:Json(name = "foo")': @@ -8 +8 - @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String + @Json(name = "foo") @field:Json(name = "foo") @get:Json(name = "foo") val value: String Autofix for src/slack/model/Example.kt line 8: Remove '@get:Json(name = "foo")': @@ -8 +8 - @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String + @Json(name = "foo") @field:Json(name = "foo") @property:Json(name = "foo") val value: String """ .trimIndent() ) } // Tweaked multiple site targets test, where we intentionally leave one for a secondary cleanup @Test fun jsonNameMultipleAllSiteTargets() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class Example( @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:8: Error: Use of site-targets on @Json are redundant. [MoshiUsageRedundantSiteTarget] @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:8: Error: Use of site-targets on @Json are redundant. [MoshiUsageRedundantSiteTarget] @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String ~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 8: Remove '@property:Json(name = "foo")': @@ -8 +8 - @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String + @field:Json(name = "foo") @get:Json(name = "foo") val value: String Autofix for src/slack/model/Example.kt line 8: Remove '@get:Json(name = "foo")': @@ -8 +8 - @field:Json(name = "foo") @property:Json(name = "foo") @get:Json(name = "foo") val value: String + @field:Json(name = "foo") @property:Json(name = "foo") val value: String """ .trimIndent() ) } @Test fun snake_case_name() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass import com.squareup.moshi.Json @JsonClass(generateAdapter = true) data class Example( val snake_case: String, @Json(name = "taken") val already_annotated_is_ignored: String ) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:8: Warning: Consider using @Json(name = ...) rather than direct snake casing. [MoshiUsageSnakeCase] val snake_case: String, ~~~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 8: Add @Json(name = "snake_case") and rename to 'snakeCase': @@ -8 +8 - val snake_case: String, + @Json(name = "snake_case") val snakeCase: String, """ .trimIndent() ) } @Test fun missing_primary() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class Example """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Error: @JsonClass-annotated types must have a primary constructor or be sealed. [MoshiUsageMissingPrimary] class Example ~~~~~~~ src/slack/model/Example.kt:6: Error: Model classes should be immutable data classes. [MoshiUsageUseData] class Example ~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) } @Test fun missing_primary_ok_in_sealed() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class Example """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun unsupportedClasses() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass class Example { @JsonClass(generateAdapter = true) annotation class UnsupportedAnnotation @JsonClass(generateAdapter = true) inner class UnsupportedInner @JsonClass(generateAdapter = true) abstract class UnsupportedAbstract @JsonClass(generateAdapter = true) interface UnsupportedInterface @JsonClass(generateAdapter = false) interface UnsupportedInterfaceButGenerateAdapterIsFalseIsOk } """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Error: This type cannot be annotated with @JsonClass. [MoshiUsageUnsupportedType] @JsonClass(generateAdapter = true) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:9: Error: This type cannot be annotated with @JsonClass. [MoshiUsageUnsupportedType] @JsonClass(generateAdapter = true) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:12: Error: This type cannot be annotated with @JsonClass. [MoshiUsageUnsupportedType] @JsonClass(generateAdapter = true) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:15: Error: This type cannot be annotated with @JsonClass. [MoshiUsageUnsupportedType] @JsonClass(generateAdapter = true) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 6: Remove '@JsonClass(generateAdapter = true)': @@ -6 +6 - @JsonClass(generateAdapter = true) Autofix for src/slack/model/Example.kt line 9: Remove '@JsonClass(generateAdapter = true)': @@ -9 +9 - @JsonClass(generateAdapter = true) Autofix for src/slack/model/Example.kt line 12: Remove '@JsonClass(generateAdapter = true)': @@ -12 +12 - @JsonClass(generateAdapter = true) Autofix for src/slack/model/Example.kt line 15: Remove '@JsonClass(generateAdapter = true)': @@ -15 +15 - @JsonClass(generateAdapter = true) """ .trimIndent() // Weirdness here because spotless strips the trailing whitespace after the '+' .lineSequence() .map { line -> if (line == "+") { "+ " } else { line } } .joinToString("\n") ) } @Test fun unsupportedClasses_okWithAdaptedBy() { val source = kotlin( """ package slack.model import androidx.annotation.Keep import com.squareup.moshi.JsonAdapter import dev.zacsweers.moshix.adapters.AdaptedBy class Example { @AdaptedBy(CustomFactory::class) annotation class UnsupportedAnnotation @AdaptedBy(CustomFactory::class) inner class UnsupportedInner @AdaptedBy(CustomFactory::class) abstract class UnsupportedAbstract @AdaptedBy(CustomFactory::class) interface UnsupportedInterface } @Keep abstract class CustomFactory : JsonAdapter.Factory """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun double_class_annotation() { val source = kotlin( """ package slack.model import androidx.annotation.Keep import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.adapters.AdaptedBy @JsonClass(generateAdapter = true) @AdaptedBy(CustomFactory::class) data class Example(val value: String) @Keep abstract class CustomFactory : JsonAdapter.Factory """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:8: Error: Only use one of @AdaptedBy or @JsonClass. [MoshiUsageDoubleClassAnnotation] @JsonClass(generateAdapter = true) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:9: Error: Only use one of @AdaptedBy or @JsonClass. [MoshiUsageDoubleClassAnnotation] @AdaptedBy(CustomFactory::class) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 8: Remove '@JsonClass(generateAdapter = true)': @@ -8 +8 - @JsonClass(generateAdapter = true) Autofix for src/slack/model/Example.kt line 9: Remove '@AdaptedBy(CustomFactory::class)': @@ -9 +9 - @AdaptedBy(CustomFactory::class) """ .trimIndent() ) } @Test fun valid_adapters() { val source = kotlin( """ package slack.model import androidx.annotation.Keep import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import dev.zacsweers.moshix.adapters.AdaptedBy @AdaptedBy(CustomFactory::class) class Example1(val value: String) @AdaptedBy(CustomAdapter::class) class Example2(val value: String) @AdaptedBy(NotAnAdapter::class) class Example3 @JsonClass(generateAdapter = true) data class Example4( @AdaptedBy(CustomAdapter::class) val value1: String, @AdaptedBy(NotAnAdapter::class) val value2: String ) @AdaptedBy(CustomAdapterMissingKeep::class) class Example5 @Keep abstract class CustomFactory : JsonAdapter.Factory @Keep abstract class CustomAdapter : JsonAdapter() class NotAnAdapter abstract class CustomAdapterMissingKeep : JsonAdapter() """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example1.kt:14: Error: @AdaptedBy.adapter must be a JsonAdapter or JsonAdapter.Factory. [MoshiUsageAdaptedByRequiresAdapter] @AdaptedBy(NotAnAdapter::class) ~~~~~~~~~~~~~~~~~~~ src/slack/model/Example1.kt:20: Error: @AdaptedBy.adapter must be a JsonAdapter or JsonAdapter.Factory. [MoshiUsageAdaptedByRequiresAdapter] @AdaptedBy(NotAnAdapter::class) val value2: String ~~~~~~~~~~~~~~~~~~~ src/slack/model/Example1.kt:23: Error: Adapters targeted by @AdaptedBy must have @Keep. [MoshiUsageAdaptedByRequiresKeep] @AdaptedBy(CustomAdapterMissingKeep::class) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } @Test fun unsupportedClasses_okWithCustomGenerator() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass class Example { @JsonClass(generateAdapter = true, generator = "custom") annotation class UnsupportedAnnotation @JsonClass(generateAdapter = true, generator = "custom") inner class UnsupportedInner @JsonClass(generateAdapter = true, generator = "custom") abstract class UnsupportedAbstract @JsonClass(generateAdapter = true, generator = "custom") interface UnsupportedInterface } """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun unsupported_visibility() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) private data class PrivateClass(val value: String) open class EnclosingClass { @JsonClass(generateAdapter = true) protected data class ProtectedClass(val value: String) } """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/PrivateClass.kt:6: Error: @JsonClass-annotated types must be public, package-private, or internal. [MoshiUsageClassVisibility] private data class PrivateClass(val value: String) ~~~~~~~ src/slack/model/PrivateClass.kt:10: Error: @JsonClass-annotated types must be public, package-private, or internal. [MoshiUsageClassVisibility] protected data class ProtectedClass(val value: String) ~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/PrivateClass.kt line 6: Make 'internal': @@ -6 +6 - private data class PrivateClass(val value: String) + internal data class PrivateClass(val value: String) Autofix for src/slack/model/PrivateClass.kt line 10: Make 'internal': @@ -10 +10 - protected data class ProtectedClass(val value: String) + internal data class ProtectedClass(val value: String) """ .trimIndent() ) } @Test fun enum_prop_suggest_moshi() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example(val value: TestEnum) enum class TestEnum { VALUE } """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Warning: Consider making enum properties also use Moshi. [MoshiUsageEnumPropertyCouldBeMoshi] data class Example(val value: TestEnum) ~~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) } @Test fun enum_prop_default_unknown() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example( val value1: TestEnum?, val value2: TestEnum? = null, val value3: TestEnum? = UNKNOWN, val value4: TestEnum = UNKNOWN, val value5: TestEnum = TestEnum.UNKNOWN, ) @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, VALUE } """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:9: Error: Suspicious default value to 'UNKNOWN' for a Moshi enum. [MoshiUsageEnumPropertyDefaultUnknown] val value3: TestEnum? = UNKNOWN, ~~~~~~~ src/slack/model/Example.kt:10: Error: Suspicious default value to 'UNKNOWN' for a Moshi enum. [MoshiUsageEnumPropertyDefaultUnknown] val value4: TestEnum = UNKNOWN, ~~~~~~~ src/slack/model/Example.kt:11: Error: Suspicious default value to 'UNKNOWN' for a Moshi enum. [MoshiUsageEnumPropertyDefaultUnknown] val value5: TestEnum = TestEnum.UNKNOWN, ~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 9: Remove ' = UNKNOWN': @@ -9 +9 - val value3: TestEnum? = UNKNOWN, + val value3: TestEnum?, Autofix for src/slack/model/Example.kt line 10: Remove ' = UNKNOWN': @@ -10 +10 - val value4: TestEnum = UNKNOWN, + val value4: TestEnum, Autofix for src/slack/model/Example.kt line 11: Remove ' = TestEnum.UNKNOWN': @@ -11 +11 - val value5: TestEnum = TestEnum.UNKNOWN, + val value5: TestEnum, """ .trimIndent() ) } @Test fun enum_prop_already_moshi() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example(val value: TestEnum) @JsonClass(generateAdapter = false) enum class TestEnum { UNKNOWN, VALUE } """ ) .indented() lint().files(*testFiles(), source).run().expectClean() } @Test fun objects_cannot_jsonClass() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) object Example """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:5: Error: Object types cannot be annotated with @JsonClass. [MoshiUsageObject] @JsonClass(generateAdapter = true) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 5: Remove '@JsonClass(generateAdapter = true)': @@ -5 +5 - @JsonClass(generateAdapter = true) """ .trimIndent() ) } @Test fun prefer_data_classes() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class Example(val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:6: Error: Model classes should be immutable data classes. [MoshiUsageUseData] class Example(val value: String) ~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun propertyTypes() { val externalType = kotlin( """ package external class ExternalType """ ) .indented() val externalTypeAnnotated = kotlin( """ package external import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ExternalTypeAnnotated(val value: String) """ ) .indented() val internalType = kotlin( """ package slack class InternalType """ ) .indented() val internalTypeAnnotated = kotlin( """ package slack import androidx.annotation.Keep import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonAdapter import dev.zacsweers.moshix.adapters.AdaptedBy @JsonClass(generateAdapter = true) data class InternalTypeAnnotated(val value: String) @AdaptedBy(InternalTypeAdapter::class) data class InternalTypeAnnotated2(val value: String) @Keep abstract class InternalTypeAdapter : JsonAdapter.Factory """ ) .indented() val jsonQualifier = kotlin( """ package com.squareup.moshi annotation class JsonQualifier """ ) .indented() val customQualifier = kotlin( """ package test import com.squareup.moshi.JsonQualifier @JsonQualifier annotation class CustomQualifier """ ) .indented() val source = kotlin( """ package slack.model import androidx.annotation.Keep import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import java.util.ArrayList import java.util.HashSet import java.util.HashMap import java.util.Date import external.ExternalType import external.ExternalTypeAnnotated import slack.InternalType import slack.InternalTypeAnnotated import slack.InternalTypeAnnotated2 import dev.zacsweers.moshix.adapters.AdaptedBy import test.CustomQualifier @JsonClass(generateAdapter = true) data class Example( // collections val okList: List, val okSet: Set, val okCollection: Collection, val okMap: Map, val concreteList: ArrayList, val concreteSet: HashSet, val concreteMap: HashMap, // platform val platformType: Date, @AdaptedBy(DateFactory::class) val adaptedByOk: Date, // external val externalType: ExternalType, val externalTypeAnnotated: ExternalTypeAnnotated, // internal val internalType: InternalType, val internalTypeAnnotated: InternalTypeAnnotated, val internalTypeAnnotated2: InternalTypeAnnotated2, val int: Int, val string: String, val nullableString: String?, val any: Any, // Arrays val arrayType: Array, val intArray: IntArray, val boolArray: BooleanArray, val complexArray: Array>, val badGeneric: List, val badGeneric2: CustomGenericType, val badNestedGeneric: CustomGenericType>, // This would normally error but since it has a custom qualifier we skip the check @CustomQualifier val customQualifier: Date, // Mutable collections val mutableList: MutableList, val mutableSet: MutableSet, val mutableCollection: MutableCollection, val mutableMap: MutableMap ) @Keep abstract class DateFactory : JsonAdapter.Factory @JsonClass(generateAdapter = true) data class CustomGenericType(val value: T) """ ) .indented() lint() .files( *testFiles(), externalType, externalTypeAnnotated, internalType, internalTypeAnnotated, jsonQualifier, customQualifier, source, ) .allowClassNameClashes(true) .run() .expect( """ src/slack/model/Example.kt:43: Warning: Prefer List over Array. [MoshiUsageArray] val arrayType: Array, ~~~~~~~~~~~~~ src/slack/model/Example.kt:44: Warning: Prefer List over Array. [MoshiUsageArray] val intArray: IntArray, ~~~~~~~~ src/slack/model/Example.kt:45: Warning: Prefer List over Array. [MoshiUsageArray] val boolArray: BooleanArray, ~~~~~~~~~~~~ src/slack/model/Example.kt:46: Warning: Prefer List over Array. [MoshiUsageArray] val complexArray: Array>, ~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:53: Error: Use immutable collections rather than mutable versions. [MoshiUsageMutableCollections] val mutableList: MutableList, ~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:54: Error: Use immutable collections rather than mutable versions. [MoshiUsageMutableCollections] val mutableSet: MutableSet, ~~~~~~~~~~~~~~~ src/slack/model/Example.kt:55: Error: Use immutable collections rather than mutable versions. [MoshiUsageMutableCollections] val mutableCollection: MutableCollection, ~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:56: Error: Use immutable collections rather than mutable versions. [MoshiUsageMutableCollections] val mutableMap: MutableMap ~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:25: Hint: Concrete Collection type 'ArrayList' is not natively supported by Moshi. [MoshiUsageNonMoshiClassCollection] val concreteList: ArrayList, ~~~~~~~~~~~~~~ src/slack/model/Example.kt:26: Hint: Concrete Collection type 'HashSet' is not natively supported by Moshi. [MoshiUsageNonMoshiClassCollection] val concreteSet: HashSet, ~~~~~~~~~~~~ src/slack/model/Example.kt:32: Error: External type 'ExternalType' is not natively supported by Moshi. [MoshiUsageNonMoshiClassExternal] val externalType: ExternalType, ~~~~~~~~~~~~ src/slack/model/Example.kt:47: Error: External type 'ExternalType' is not natively supported by Moshi. [MoshiUsageNonMoshiClassExternal] val badGeneric: List, ~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:48: Error: External type 'ExternalType' is not natively supported by Moshi. [MoshiUsageNonMoshiClassExternal] val badGeneric2: CustomGenericType, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:49: Error: External type 'ExternalType' is not natively supported by Moshi. [MoshiUsageNonMoshiClassExternal] val badNestedGeneric: CustomGenericType>, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:35: Hint: Non-Moshi internal type 'InternalType' is not natively supported by Moshi. [MoshiUsageNonMoshiClassInternal] val internalType: InternalType, ~~~~~~~~~~~~ src/slack/model/Example.kt:27: Hint: Concrete Map type 'HashMap' is not natively supported by Moshi. [MoshiUsageNonMoshiClassMap] val concreteMap: HashMap, ~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:29: Warning: Platform type 'Date' is not natively supported by Moshi. [MoshiUsageNonMoshiClassPlatform] val platformType: Date, ~~~~ 8 errors, 5 warnings, 4 hints """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 43: Change to List: @@ -43 +43 - val arrayType: Array, + val arrayType: List, Autofix for src/slack/model/Example.kt line 44: Change to List: @@ -44 +44 - val intArray: IntArray, + val intArray: List, Autofix for src/slack/model/Example.kt line 45: Change to List: @@ -45 +45 - val boolArray: BooleanArray, + val boolArray: List, Autofix for src/slack/model/Example.kt line 46: Change to List>: @@ -46 +46 - val complexArray: Array>, + val complexArray: List>, Autofix for src/slack/model/Example.kt line 53: Change to List: @@ -53 +53 - val mutableList: MutableList, + val mutableList: List, Autofix for src/slack/model/Example.kt line 54: Change to Set: @@ -54 +54 - val mutableSet: MutableSet, + val mutableSet: Set, Autofix for src/slack/model/Example.kt line 55: Change to Collection: @@ -55 +55 - val mutableCollection: MutableCollection, + val mutableCollection: Collection, Autofix for src/slack/model/Example.kt line 56: Change to Map: @@ -56 +56 - val mutableMap: MutableMap + val mutableMap: Map Autofix for src/slack/model/Example.kt line 25: Change to List: @@ -25 +25 - val concreteList: ArrayList, + val concreteList: List, Autofix for src/slack/model/Example.kt line 26: Change to Set: @@ -26 +26 - val concreteSet: HashSet, + val concreteSet: Set, Autofix for src/slack/model/Example.kt line 27: Change to Map: @@ -27 +27 - val concreteMap: HashMap, + val concreteMap: Map, """ .trimIndent() ) } @Test fun kotlin_jsonQualifierAnnotation_ok() { val source = kotlin( """ package slack.model import com.squareup.moshi.JsonQualifier import kotlin.annotation.Retention import kotlin.annotation.AnnotationRetention import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.PROPERTY import kotlin.annotation.AnnotationTarget import kotlin.annotation.AnnotationTarget.FIELD @JsonQualifier annotation class NoAnnotationsIsOk @Target(FIELD) @JsonQualifier annotation class NoRetentionIsOk @Target(AnnotationRetention.FIELD) @Retention(AnnotationRetention.RUNTIME) @JsonQualifier annotation class CorrectTargetAndRetention @Target(PROPERTY, AnnotationRetention.FIELD) @Retention(RUNTIME) @JsonQualifier annotation class CorrectTargetAndRetention2 @Target([PROPERTY, FIELD]) @Retention(RUNTIME) @JsonQualifier annotation class CorrectTargetAndRetention3 @Target(PROPERTY) @JsonQualifier annotation class MissingTarget @Retention(AnnotationRetention.BINARY) @JsonQualifier annotation class WrongRetention """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/NoAnnotationsIsOk.kt:37: Error: JsonQualifiers must have RUNTIME retention. [MoshiUsageQualifierRetention] @Retention(AnnotationRetention.BINARY) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/NoAnnotationsIsOk.kt:33: Error: JsonQualifiers must include FIELD targeting. [MoshiUsageQualifierTarget] @Target(PROPERTY) ~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/NoAnnotationsIsOk.kt line 37: Remove '@Retention(AnnotationRetention.BINARY)': @@ -37 +37 - @Retention(AnnotationRetention.BINARY) """ .trimIndent() ) } @Test fun java_jsonQualifierAnnotation_ok() { val source = java( """ package slack.model; import com.squareup.moshi.JsonQualifier; import java.lang.annotation.ElementType; import static java.lang.annotation.ElementType.FIELD; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @JsonQualifier public @interface NoTargetIsOk {} @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JsonQualifier public @interface CorrectTargetAndRetention {} @Target({FIELD, ElementType.METHOD}) @Retention(RUNTIME) @JsonQualifier public @interface CorrectTargetAndRetention2 {} @Target(ElementType.METHOD) @Retention(RUNTIME) @JsonQualifier public @interface MissingField {} @Target(FIELD) @Retention(RetentionPolicy.CLASS) @JsonQualifier public @interface WrongRetention {} @Target(FIELD) @JsonQualifier public @interface MissingRetention {} """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/NoTargetIsOk.java:31: Error: JsonQualifiers must have RUNTIME retention. [MoshiUsageQualifierRetention] @Retention(RetentionPolicy.CLASS) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/NoTargetIsOk.java:37: Error: JsonQualifiers must have RUNTIME retention. [MoshiUsageQualifierRetention] public @interface MissingRetention {} ~~~~~~~~~~~~~~~~ src/slack/model/NoTargetIsOk.java:25: Error: JsonQualifiers must include FIELD targeting. [MoshiUsageQualifierTarget] @Target(ElementType.METHOD) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/NoTargetIsOk.java line 31: Replace with RUNTIME: @@ -31 +31 - @Retention(RetentionPolicy.CLASS) + @Retention(RetentionPolicy.RUNTIME) """ .trimIndent() ) } @Test fun redundantJsonName() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example(@Json(name = "value") val value: String) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:7: Warning: Json.name with the same value as the property/enum member name is redundant. [MoshiUsageRedundantJsonName] data class Example(@Json(name = "value") val value: String) ~~~~~~~ 0 errors, 1 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 7: Remove '@Json(name = "value")': @@ -7 +7 - data class Example(@Json(name = "value") val value: String) @@ -8 +7 + data class Example( val value: String) """ .trimIndent() ) } @Test fun serializedNameIssues() { val serializedName = java( """ package com.google.gson.annotations; public @interface SerializedName { String value(); String[] alternate() default {}; } """ .trimIndent() ) val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.google.gson.annotations.SerializedName @JsonClass(generateAdapter = true) data class Example( val noAnnotations: String, @Json(name = "full_moshi") val fullMoshi: String, @SerializedName("full_gson") val fullGson: String, @SerializedName("full_gson_alts", alternate = ["foo"]) val fullGsonAlternates: String, @Json(name = "mixed") @SerializedName("mixed") val mixedSame: String, @Json(name = "mixed_diff") @SerializedName("mixed_diff_2") val mixedDiff: String, @Json(name = "mixed_alts") @SerializedName("mixed_alts", alternate = ["foo"]) val mixedAlternates: String, ) """ ) .indented() lint() .files(*testFiles(), serializedName, source) .run() .expect( """ src/slack/model/Example.kt:11: Error: Use Moshi's @Json rather than Gson's @SerializedName. [MoshiUsageSerializedName] @SerializedName("full_gson") val fullGson: String, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:12: Error: Use Moshi's @Json rather than Gson's @SerializedName. [MoshiUsageSerializedName] @SerializedName("full_gson_alts", alternate = ["foo"]) val fullGsonAlternates: String, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:13: Error: Use Moshi's @Json rather than Gson's @SerializedName. [MoshiUsageSerializedName] @Json(name = "mixed") @SerializedName("mixed") val mixedSame: String, ~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:14: Error: Use Moshi's @Json rather than Gson's @SerializedName. [MoshiUsageSerializedName] @Json(name = "mixed_diff") @SerializedName("mixed_diff_2") val mixedDiff: String, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/model/Example.kt:15: Error: Use Moshi's @Json rather than Gson's @SerializedName. [MoshiUsageSerializedName] @Json(name = "mixed_alts") @SerializedName("mixed_alts", alternate = ["foo"]) val mixedAlternates: String, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/model/Example.kt line 11: Replace with @Json(name = "full_gson"): @@ -11 +11 - @SerializedName("full_gson") val fullGson: String, + @Json(name = "full_gson") val fullGson: String, Autofix for src/slack/model/Example.kt line 13: Remove '@SerializedName("mixed")': @@ -13 +13 - @Json(name = "mixed") @SerializedName("mixed") val mixedSame: String, + @Json(name = "mixed") val mixedSame: String, """ .trimIndent() ) } @Test fun duplicateNames() { val source = kotlin( """ package slack.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Example( val value: String, @Json(name = "value") val anotherValue: String, @Json(name = "value2") val anotherValue2: String, @Json(name = "value2") val anotherValue3: String ) """ ) .indented() lint() .files(*testFiles(), source) .run() .expect( """ src/slack/model/Example.kt:8: Error: Name 'value' is duplicated by member 'anotherValue'. [MoshiUsageDuplicateJsonName] val value: String, ~~~~~ src/slack/model/Example.kt:9: Error: Name 'value' is duplicated by member 'value'. [MoshiUsageDuplicateJsonName] @Json(name = "value") val anotherValue: String, ~~~~~~~~~~~~ src/slack/model/Example.kt:10: Error: Name 'value2' is duplicated by member 'anotherValue3'. [MoshiUsageDuplicateJsonName] @Json(name = "value2") val anotherValue2: String, ~~~~~~~~~~~~~ src/slack/model/Example.kt:11: Error: Name 'value2' is duplicated by member 'anotherValue2'. [MoshiUsageDuplicateJsonName] @Json(name = "value2") val anotherValue3: String ~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } private fun testFiles() = arrayOf( keepAnnotation, jsonClassAnnotation, jsonAnnotation, jsonQualifierAnnotation, typeLabel, defaultObject, adaptedBy, jsonAdapter, ) } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/MustUseNamedParamsDetectorTest.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test class MustUseNamedParamsDetectorTest : BaseSlackLintTest() { private companion object { private val mustUseNamedParams = kotlin( """ package slack.lint.annotations /** * Callers to this function must named all parameters. */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class MustUseNamedParams """ ) .indented() } override fun getDetector(): Detector = MustUseNamedParamsDetector() override fun getIssues(): List = listOf(MustUseNamedParamsDetector.ISSUE) @Test fun simpleTest() { lint() .files( mustUseNamedParams, kotlin( """ package foo import slack.lint.annotations.MustUseNamedParams class TestFile { @MustUseNamedParams fun methodWithAnnotation(name: String) { // Do nothing. } fun methodWithoutAnnotation(name: String) { // Do nothing. } fun useMethod() { methodWithAnnotation("Zac") methodWithAnnotation(name = "Sean") methodWithoutAnnotation("Yifan") methodWithoutAnnotation(name = "Sean2") } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/TestFile.kt:16: Error: Calls to @MustUseNamedParams-annotated methods must name all parameters. [MustUseNamedParams] methodWithAnnotation("Zac") ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/NonKotlinPairDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.detector.api.Detector import org.junit.Test class NonKotlinPairDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = NonKotlinPairDetector() override fun getIssues() = NonKotlinPairDetector.issues.toList() @Test fun `Java - AndroidX Pair create shows warning`() { lint() .files( ANDROIDX_PAIR_STUB, java( """ package slack.test; import androidx.core.util.Pair; public class TestClass { public void doStuff() { new Integer(5); new String("TestString").toString(); Pair pair = Pair.create("first", "second"); pair.first.toString(); } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.java:10: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] Pair pair = Pair.create("first", "second"); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } @Test fun `Java - AndroidX Pair constructor create shows warning`() { lint() .files( ANDROIDX_PAIR_STUB, java( """ package slack.test; import androidx.core.util.Pair; public class TestClass { public void doStuff() { new Integer(5); new String("TestString").toString(); new Pair<>("first", "second").first.toString(); } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.java:10: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] new Pair<>("first", "second").first.toString(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } @Test fun `Java - Slack commons Pair constructor create shows warning`() { lint() .files( SLACK_COMMONS_PAIR, java( """ package slack.test; import slack.commons.Pair; public class TestClass { public void doStuff() { new Integer(5); new String("TestString").toString(); new Pair<>("first", "second").getFirst(); } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.java:10: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] new Pair<>("first", "second").getFirst(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } @Test fun `Java - Full qualified Slack commons Pair constructor create shows warning`() { lint() .files( SLACK_COMMONS_PAIR, java( """ package slack.test; public class TestClass { public void doStuff() { new Integer(5); new String("TestString").toString(); new slack.commons.Pair<>("first", "second").getFirst(); } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.java:8: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] new slack.commons.Pair<>("first", "second").getFirst(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } @Test fun `Kotlin - Kotlin Pair constructor create has no warnings`() { lint() .files( kotlin( """ package slack.test class TestClass { fun doStuff() { String("TestString").toString() Integer(6) val pair = Pair("first", "second") } } """ ) .indented() ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expectClean() } @Test fun `Kotlin - AndroidX Pair create shows warning`() { lint() .files( ANDROIDX_PAIR_STUB, kotlin( """ package slack.test import androidx.core.util.Pair class TestClass { fun doStuff() { Integer(5) String("TestString").toString() val pair = Pair.create("first", "second") } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.kt:10: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] val pair = Pair.create("first", "second") ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } @Test fun `Kotlin - AndroidX Pair constructor create shows warning`() { lint() .files( ANDROIDX_PAIR_STUB, kotlin( """ package slack.test import androidx.core.util.Pair class TestClass { fun doStuff() { Integer(5) String("TestString").toString() val pair = Pair("first", "second") } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.kt:10: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] val pair = Pair("first", "second") ~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } @Test fun `Kotlin - Slack commons Pair constructor create shows warning`() { lint() .files( SLACK_COMMONS_PAIR, kotlin( """ package slack.test import slack.commons.Pair class TestClass { fun doStuff() { Integer(5) Integer(6) String("TestString").toString() val pair = Pair("first", "second") } } """ ) .indented(), ) .issues(*NonKotlinPairDetector.issues.toTypedArray()) .run() .expect( """ src/slack/test/TestClass.kt:11: Warning: Use Kotlin's kotlin.Pair instead of other Pair types from other libraries like AndroidX and Slack commons [KotlinPairNotCreated] val pair = Pair("first", "second") ~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ ) } companion object { private val ANDROIDX_PAIR_STUB = java( """ package androidx.core.util; public class Pair { public final F first; public final S second; public Pair(F first, S second) { this.first = first; this.second = second; } public static Pair create(A a, B b) { return null; } } """ ) private val SLACK_COMMONS_PAIR = kotlin( """ package slack.commons data class Pair( val first: A, val second: B ) """ ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/NotNullOperatorDetectorTest.kt ================================================ // Copyright (C) 2024 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import kotlin.text.trimIndent import org.junit.Test class NotNullOperatorDetectorTest : BaseSlackLintTest() { override fun getDetector() = NotNullOperatorDetector() override fun getIssues() = listOf(NotNullOperatorDetector.ISSUE) @Test fun testDocumentationExample() { lint() .files( kotlin( """ package foo class Test { fun doNothing(t: String?): Boolean { t/* this is legal */!!.length return t!!.length == 1 } } """ ) ) .run() .expect( """ src/foo/Test.kt:6: Warning: Avoid using the !! operator [AvoidUsingNotNullOperator] t/* this is legal */!!.length ~~ src/foo/Test.kt:7: Warning: Avoid using the !! operator [AvoidUsingNotNullOperator] return t!!.length == 1 ~~ 0 errors, 2 warnings """ .trimIndent() ) } @Test fun `test clean`() { lint() .files( kotlin( """ package foo class Test { fun doNothing(t: String?): Boolean { // Should not match despite the !! string in it "!!".length++ return t?.length == 1 } } """ ) ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/NullableConcurrentHashMapDetectorTest.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class NullableConcurrentHashMapDetectorTest : BaseSlackLintTest() { override fun getDetector() = NullableConcurrentHashMapDetector() override fun getIssues() = listOf(NullableConcurrentHashMapDetector.ISSUE) @Test fun concurrentHashMapWithNonNullableTypes() { lint() .files( kotlin( """ fun test() { val map = java.util.concurrent.ConcurrentHashMap() val map2: ConcurrentHashMap = java.util.concurrent.ConcurrentHashMap() } """ ) ) .run() .expectClean() } @Test fun concurrentHashMapWithNullableKeyType() { lint() .files( kotlin( """ fun test() { val map = java.util.concurrent.ConcurrentHashMap() val map2: java.util.concurrent.ConcurrentHashMap = java.util.concurrent.ConcurrentHashMap() } """ ) ) .run() .expect( """ src/test.kt:3: Error: ConcurrentHashMap should not use nullable key types [NullableConcurrentHashMap] val map = java.util.concurrent.ConcurrentHashMap() ~~~~~~~ src/test.kt:4: Error: ConcurrentHashMap should not use nullable key types [NullableConcurrentHashMap] val map2: java.util.concurrent.ConcurrentHashMap = java.util.concurrent.ConcurrentHashMap() ~~~~~~~ 2 errors """ .trimIndent() ) } @Test fun concurrentHashMapWithNullableValueType() { lint() .files( kotlin( """ fun test() { val map = java.util.concurrent.ConcurrentHashMap() } """ ) ) .run() .expect( """ src/test.kt:3: Error: ConcurrentHashMap should not use nullable value types [NullableConcurrentHashMap] val map = java.util.concurrent.ConcurrentHashMap() ~~~~ 1 error """ .trimIndent() ) } @Test fun concurrentHashMapWithBothNullableTypes() { lint() .files( kotlin( """ fun test() { val map = java.util.concurrent.ConcurrentHashMap() } """ ) ) .run() .expect( """ src/test.kt:3: Error: ConcurrentHashMap should not use nullable key types [NullableConcurrentHashMap] val map = java.util.concurrent.ConcurrentHashMap() ~~~~~~~ src/test.kt:3: Error: ConcurrentHashMap should not use nullable value types [NullableConcurrentHashMap] val map = java.util.concurrent.ConcurrentHashMap() ~~~~ 2 errors """ .trimIndent() ) } @Test fun javaConcurrentHashMapWithNullableAnnotation() { lint() .files( java( """ package test; import java.lang.annotation.ElementType; import java.lang.annotation.Target; import java.util.concurrent.ConcurrentHashMap; public class Test { @Target(ElementType.TYPE) public @interface Nullable {} public void test() { var map = new ConcurrentHashMap<@Nullable String, Integer>(); ConcurrentHashMap<@Nullable String, Integer> map2 = new ConcurrentHashMap<>(); } } """ ) ) .run() .expect( """ src/test/Test.java:13: Error: ConcurrentHashMap should not use nullable key types [NullableConcurrentHashMap] var map = new ConcurrentHashMap<@Nullable String, Integer>(); ~~~~~~~~~~~~~~~~ src/test/Test.java:14: Error: ConcurrentHashMap should not use nullable key types [NullableConcurrentHashMap] ConcurrentHashMap<@Nullable String, Integer> map2 = new ConcurrentHashMap<>(); ~~~~~~~~~~~~~~~~ 2 errors """ .trimIndent() ) } @Test fun concurrentHashMapDeclaredSeparately() { lint() .files( kotlin( """ class TestClass { private val map: java.util.concurrent.ConcurrentHashMap init { map = java.util.concurrent.ConcurrentHashMap() } } """ ) ) .run() .expectClean() } @Test fun concurrentHashMapDeclaredSeparatelyWithNullableTypes() { lint() .files( kotlin( """ class TestClass { private val map: java.util.concurrent.ConcurrentHashMap init { map = java.util.concurrent.ConcurrentHashMap() } } """ ) ) .run() .expect( """ src/TestClass.kt:3: Error: ConcurrentHashMap should not use nullable key types [NullableConcurrentHashMap] private val map: java.util.concurrent.ConcurrentHashMap ~~~~~~~ src/TestClass.kt:3: Error: ConcurrentHashMap should not use nullable value types [NullableConcurrentHashMap] private val map: java.util.concurrent.ConcurrentHashMap ~~~~ 2 errors """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/RawDispatchersUsageDetectorTest.kt ================================================ // Copyright (C) 2020 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class RawDispatchersUsageDetectorTest : BaseSlackLintTest() { companion object { // Stub of dispatchers. // Use a combination of string constants and getters to ensure coverage for both // Also add an extension function to check we don't try to lint those. // language=kotlin private val DISPATCHERS_STUB = kotlin( """ package kotlinx.coroutines object Dispatchers { @JvmStatic val Default: String get() = error() @JvmStatic val Main: String get() = error() @JvmStatic val Unconfined: String = "" @JvmStatic val IO: String = "" } fun Dispatchers.someExtension() { } """ .trimIndent() ) } override fun getDetector() = RawDispatchersUsageDetector() override fun getIssues() = listOf(RawDispatchersUsageDetector.ISSUE) @Test fun simple() { lint() .files( DISPATCHERS_STUB, kotlin( """ package test.pkg import kotlinx.coroutines.Dispatchers fun example() { Dispatchers.IO Dispatchers.Default Dispatchers.Unconfined Dispatchers.Main Dispatchers.someExtension() Dispatchers::IO Dispatchers::Default Dispatchers::Unconfined Dispatchers::Main } """ .trimIndent() ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/test/pkg/test.kt:6: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers.IO ~~~~~~~~~~~~~~ src/test/pkg/test.kt:7: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers.Default ~~~~~~~~~~~~~~~~~~~ src/test/pkg/test.kt:8: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers.Unconfined ~~~~~~~~~~~~~~~~~~~~~~ src/test/pkg/test.kt:9: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers.Main ~~~~~~~~~~~~~~~~ src/test/pkg/test.kt:11: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers::IO ~~~~~~~~~~~~~~~ src/test/pkg/test.kt:12: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers::Default ~~~~~~~~~~~~~~~~~~~~ src/test/pkg/test.kt:13: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers::Unconfined ~~~~~~~~~~~~~~~~~~~~~~~ src/test/pkg/test.kt:14: Error: Use SlackDispatchers. [RawDispatchersUse] Dispatchers::Main ~~~~~~~~~~~~~~~~~ 8 errors, 0 warnings """ .trimIndent() ) } // Usage in tests are fine @Test fun testsAreFine() { lint() .files( DISPATCHERS_STUB, kotlin( "test/test/pkg/Test.kt", """ package test.pkg import kotlinx.coroutines.Dispatchers fun example() { Dispatchers.IO Dispatchers.Default Dispatchers.Unconfined Dispatchers.Main Dispatchers.someExtension() Dispatchers::IO Dispatchers::Default Dispatchers::Unconfined Dispatchers::Main } """ .trimIndent(), ) .indented(), ) .allowCompilationErrors(false) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/RedactedUsageDetectorTest.kt ================================================ // Copyright (C) 2020 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class RedactedUsageDetectorTest : BaseSlackLintTest() { companion object { private val REDACTED_STUB = kotlin( """ package slack.annotations annotation class Redacted annotation class AnotherRedacted annotation class AnotherAnnotation """ .trimIndent() ) } override fun getDetector() = RedactedUsageDetector() override fun getIssues() = RedactedUsageDetector.ISSUES.toList() @Test fun smokeTest() { lint() .files( REDACTED_STUB, kotlin( """ package test.pkg import slack.annotations.Redacted import slack.annotations.AnotherRedacted import slack.annotations.AnotherAnnotation @Redacted data class RedactedClass(val value: String) data class RedactedProps(@Redacted val value: String) """ ) .indented(), java( """ package test.pkg; import slack.annotations.Redacted; import slack.annotations.AnotherAnnotation; @Redacted class RedactedClass { @Redacted int value; @AnotherAnnotation int value2; @Redacted public int getValue() { return value; } @AnotherAnnotation public int getValue2() { return value2; } @AnotherAnnotation static class AnotherInner { } } """ ) .indented(), java( """ package test.pkg; import slack.annotations.Redacted; import slack.annotations.AnotherRedacted; @AnotherRedacted class AnotherRedactedClass { @AnotherRedacted int value; @AnotherRedacted public int getValue() { return value; } } """ ) .indented(), ) .allowCompilationErrors(false) .allowClassNameClashes(true) .run() .expect( """ src/test/pkg/AnotherRedactedClass.java:6: Error: @Redacted is only supported in Kotlin classes! [RedactedInJavaUsage] @AnotherRedacted ~~~~~~~~~~~~~~~~ src/test/pkg/AnotherRedactedClass.java:8: Error: @Redacted is only supported in Kotlin classes! [RedactedInJavaUsage] @AnotherRedacted ~~~~~~~~~~~~~~~~ src/test/pkg/AnotherRedactedClass.java:11: Error: @Redacted is only supported in Kotlin classes! [RedactedInJavaUsage] @AnotherRedacted ~~~~~~~~~~~~~~~~ src/test/pkg/RedactedClass.java:6: Error: @Redacted is only supported in Kotlin classes! [RedactedInJavaUsage] @Redacted ~~~~~~~~~ src/test/pkg/RedactedClass.java:8: Error: @Redacted is only supported in Kotlin classes! [RedactedInJavaUsage] @Redacted ~~~~~~~~~ src/test/pkg/RedactedClass.java:13: Error: @Redacted is only supported in Kotlin classes! [RedactedInJavaUsage] @Redacted ~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/RestrictCallsToDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class RestrictCallsToDetectorTest : BaseSlackLintTest() { private companion object { private val restrictCallsTo = kotlin( """ package slack.lint.annotations import java.lang.annotation.Inherited @Inherited annotation class RestrictCallsTo(val scope: Int) { companion object { const val FILE = 0 } } """ ) .indented() } override fun getDetector() = RestrictCallsToDetector() override fun getIssues() = listOf(RestrictCallsToDetector.ISSUE) @Test fun smokeTest() { lint() .files( restrictCallsTo, kotlin( """ package foo import slack.lint.annotations.RestrictCallsTo import slack.lint.annotations.RestrictCallsTo.Companion.FILE interface MyApi { fun example() @RestrictCallsTo(FILE) fun annotatedExample() } class SameFile { fun doStuffWith(api: MyApi) { // This is ok api.example() api.annotatedExample() } } class MyApiImpl : MyApi { override fun example() { annotatedExample() } // Note this is not annotated, ensures we check up the hierarchy override fun annotatedExample() { println("Hello") } } """ ) .indented(), kotlin( """ package foo class DifferentFile { fun doStuffWith(api: MyApi) { // This is ok api.example() // This is not api.annotatedExample() } } class MyApiImpl2 : MyApi { override fun example() { // Not ok annotatedExample() } // Still ok override fun annotatedExample() { println("Hello") } fun backdoor() { // Backdoors don't work either! This isn't annotated on the impl but we check the // original overridden type. MyApiImpl().annotatedExample() } } """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/foo/DifferentFile.kt:8: Error: Methods annotated with @RestrictedCallsTo should only be called from the specified scope. [RestrictCallsTo] api.annotatedExample() ~~~~~~~~~~~~~~~~~~~~~~ src/foo/DifferentFile.kt:15: Error: Methods annotated with @RestrictedCallsTo should only be called from the specified scope. [RestrictCallsTo] annotatedExample() ~~~~~~~~~~~~~~~~~~ src/foo/DifferentFile.kt:26: Error: Methods annotated with @RestrictedCallsTo should only be called from the specified scope. [RestrictCallsTo] MyApiImpl().annotatedExample() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/SerializableDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class SerializableDetectorTest : BaseSlackLintTest() { override fun getDetector() = SerializableDetector() override fun getIssues() = listOf(SerializableDetector.ISSUE) @Test fun kotlin_happyPath() { lint() .detector(RawDispatchersUsageDetector()) .issues(RawDispatchersUsageDetector.ISSUE) .files( kotlin( """ package slack class ImplementsNothing """ ) .indented() ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun kotlin_happyPath_implementsBoth() { lint() .files( kotlin( """ package slack import java.io.Serializable import android.os.Parcelable class ImplementsBoth : Serializable, Parcelable """ ) .indented() ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun kotlin_implicitPlatformType_isIgnored() { lint() .files( kotlin( """ package slack import kotlin.RuntimeException class ImplementsImplicitly : RuntimeException """ ) .indented() ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun kotlin_explicitPlatformType_isNotIgnored() { lint() .files( kotlin( """ package slack import java.io.Serializable import kotlin.RuntimeException class ImplementsExplicitly : RuntimeException, Serializable """ ) .indented() ) .allowCompilationErrors(false) .run() .expect( """ src/slack/ImplementsExplicitly.kt:6: Error: Don't use Serializable. [SerializableUsage] class ImplementsExplicitly : RuntimeException, Serializable ~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlin_failure() { lint() .files( kotlin( """ package slack import java.io.Serializable class BadClass : Serializable """ ) .indented() ) .allowCompilationErrors(false) .run() .expect( """ src/slack/BadClass.kt:5: Error: Don't use Serializable. [SerializableUsage] class BadClass : Serializable ~~~~~~~~ 1 errors, 0 warnings """ ) } @Test fun kotlin_failure_kotlin_io() { lint() .files( kotlin( """ package slack import kotlin.io.Serializable class BadClass : Serializable """ ) .indented() ) .allowCompilationErrors(false) .run() .expect( """ src/slack/BadClass.kt:5: Error: Don't use Serializable. [SerializableUsage] class BadClass : Serializable ~~~~~~~~ 1 errors, 0 warnings """ ) } @Test fun java_happyPath() { lint() .detector(RawDispatchersUsageDetector()) .issues(RawDispatchersUsageDetector.ISSUE) .files( java( """ package slack; class ImplementsNothing { } """ ) .indented() ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun java_happyPath_implementsBoth() { lint() .files( java( """ package slack; import java.io.Serializable; import android.os.Parcelable; class ImplementsBoth implements Serializable, Parcelable { } """ ) .indented() ) .allowCompilationErrors(false) .run() .expectClean() } @Test fun java_failure() { lint() .files( java( """ package slack; import java.io.Serializable; class BadClass implements Serializable { } """ ) .indented() ) .allowCompilationErrors(false) .run() .expect( """ src/slack/BadClass.java:5: Error: Don't use Serializable. [SerializableUsage] class BadClass implements Serializable { ~~~~~~~~ 1 errors, 0 warnings """ ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/TestParameterSiteTargetDetectorTest.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import org.junit.Test class TestParameterSiteTargetDetectorTest : BaseSlackLintTest() { override fun getDetector() = TestParameterSiteTargetDetector() override fun getIssues() = listOf(TestParameterSiteTargetDetector.ISSUE) @Test fun testParameterWithParamSiteTarget() { lint() .files( kotlin( """ class MyTest( @param:com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ) """ ) ) .run() .expectClean() } @Test fun testParameterWithoutParamSiteTarget() { lint() .files( kotlin( """ class MyTest( @com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ) """ ) ) .run() .expect( """ src/MyTest.kt:3: Error: TestParameter annotation has the wrong site target [TestParameterSiteTarget] @com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 error """ .trimIndent() ) } @Test fun testParameterWithWrongSiteTarget() { lint() .files( kotlin( """ class MyTest( @field:com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ) """ ) ) .run() .expect( """ src/MyTest.kt:3: Error: TestParameter annotation has the wrong site target [TestParameterSiteTarget] @field:com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 error """ .trimIndent() ) } @Test fun testNonPropertyParameter() { lint() .files( kotlin( """ class MyTest( @com.google.testing.junit.testparameterinjector.TestParameter myParam: String ) """ ) ) .run() .expectClean() } @Test fun testRegularProperty() { lint() .files( kotlin( """ class MyTest { @com.google.testing.junit.testparameterinjector.TestParameter val myParam: String = "" } """ ) ) .run() .expectClean() } @Test fun testDocumentationExample() { lint() .files( kotlin( """ class MyTest( @com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ) """ ) ) .run() .expect( """ src/MyTest.kt:3: Error: TestParameter annotation has the wrong site target [TestParameterSiteTarget] @com.google.testing.junit.testparameterinjector.TestParameter val myParam: String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 error """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/ViewContextDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test class ViewContextDetectorTest : BaseSlackLintTest() { override fun lint(): TestLintTask { return super.lint().allowClassNameClashes(true) } @Test fun test_customViewInternalCaller() { lint() .files(loadStub("ViewContextDetectorTestCustomViewInternalCaller.java")) .run() .expect( """ src/main/java/ViewContextDetectorTestCustomViewInternalCaller.java:11: Error: Unsafe cast of Context to Activity [CastingViewContextToActivity] Activity a = (Activity) getContext(); ~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun test_externalCallerOnView() { lint() .files(loadStub("ViewContextDetectorTestExternalCallerOnView.java")) .run() .expect( """ src/main/java/ViewContextDetectorTestExternalCallerOnView.java:12: Error: Unsafe cast of Context to Activity [CastingViewContextToActivity] Activity activity = (Activity) view.getContext(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun test_externalCallerOnCustomView() { lint() .files(loadStub("ViewContextDetectorTestExternalCallerOnCustomView.java")) .run() .expect( """ src/main/java/ViewContextDetectorTestExternalCallerOnCustomView.java:12: Error: Unsafe cast of Context to Activity [CastingViewContextToActivity] Activity activity = (Activity) view.getContext(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun test_contentProvider() { lint().files(loadStub("ViewContextDetectorTestContentProvider.java")).run().expectClean() } override fun getDetector(): Detector { return ViewContextDetector() } override fun getIssues(): List { return ViewContextDetector.issues } /*@Test // need to improve detector to catch errors when the cast occurs later. public void test_shouldFailButDoesNot() { lint() .files( java("" + "package foo;\n" + "import android.app.Activity;\n" + "import android.content.Context;\n" + "import android.util.AttributeSet;\n" + "import android.widget.TextView;\n" + "public class Example {\n" + " TextView view;\n" + " public Example(TextView v) {\n" + " view = v;\n" + " }\n" + " public void bar() {\n" + " Context context = view.getContext();\n" + " Activity a = (Activity) context;\n" + " }\n" + "}\n" ) ) .issues(ViewContextDetector.ISSUE_VIEW_CONTEXT_CAST) .run() .expectClean(); }*/ } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/denylistedapis/DenyListedApiDetectorTest.kt ================================================ // Copyright Square, Inc. // Apache-2.0 package slack.lint.denylistedapis import com.android.tools.lint.checks.infrastructure.TestMode.Companion.FULLY_QUALIFIED import com.android.tools.lint.checks.infrastructure.TestMode.Companion.IMPORT_ALIAS import com.android.tools.lint.detector.api.Detector import org.junit.Ignore import org.junit.Test import slack.lint.BaseSlackLintTest // Adapted from https://gist.github.com/JakeWharton/19b1e7b8d5c648b2935ba89148b791ed class DenyListedApiDetectorTest : BaseSlackLintTest() { override fun getIssues() = DenyListedApiDetector.ISSUES override fun getDetector(): Detector = DenyListedApiDetector() @Test fun `flag function with params in deny list`() { lint() .files( CONTEXT_COMPAT_STUB, kotlin( """ package foo import android.content.Context import android.graphics.drawable.Drawable import androidx.core.content.ContextCompat class SomeView(context: Context) { init { ContextCompat.getDrawable(context, 42) } } """ ) .indented(), ) .run() .expect( """ src/foo/SomeView.kt:9: Error: Use Context#getDrawableCompat() instead [DenyListedApi] ContextCompat.getDrawable(context, 42) ~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun `allow function not in deny list`() { lint() .files( OBSERVABLE_STUB, TEST_OBSERVER_STUB, RX_RULE_STUB, kotlin( """ package cash import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.observers.TestObserver import com.squareup.util.rx3.test.test import com.squareup.util.rx3.test.RxRule class FooTest { @get:Rule val rxRule = RxRule() fun test() { observable().test(rxRule).assertValue(42) } fun observable(): Observable = TODO() } """ ) .indented(), ) .run() .expectClean() } @Test fun `setOnClickListener with null argument in deny list`() { lint() .files( kotlin( """ package foo import android.view.View; class SomeView(view: View) { init { view.setOnClickListener(null); } } """ ) .indented() ) .run() .expect( """ src/foo/SomeView.kt:7: Error: This fails to also set View#isClickable. Use View#clearOnClickListener() instead [DenyListedApi] view.setOnClickListener(null); ~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun `setOnClickListener with non-null argument not in deny list`() { lint() .files( kotlin( """ package foo import android.view.View; class SomeView(view: View) { init { view.setOnClickListener { // do something } } } """ ) .indented() ) .run() .expectClean() } @Test fun `setId with explicit id not in deny list`() { lint() .files( VIEWPAGER2_STUB, kotlin( """ package foo import androidx.viewpager2.widget.ViewPager2; class SomeView(view: ViewPager2) { init { view.setId(1) } } """ ) .indented(), ) .run() .expectClean() } @Test fun `setId with ViewCompat#generateViewId() in deny list`() { lint() .files( VIEWCOMPAT_STUB, VIEWPAGER2_STUB, kotlin( """ package foo import androidx.viewpager2.widget.ViewPager2; import androidx.core.view.ViewCompat; class SomeView(view: ViewPager2) { init { view.setId(ViewCompat.generateViewId()) } } """ ) .indented(), ) .skipTestModes(FULLY_QUALIFIED, IMPORT_ALIAS) // TODO relies on non-qualified matching. .run() .expect( """ src/foo/SomeView.kt:8: Error: Use an id defined in resources or a statically created instead of generating with ViewCompat.generateViewId(). See https://issuetracker.google.com/issues/185820237 [DenyListedApi] view.setId(ViewCompat.generateViewId()) ~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun `setId with View#generateViewId() in deny list`() { lint() .files( VIEWPAGER2_STUB, kotlin( """ package foo import androidx.viewpager2.widget.ViewPager2; import android.view.View; class SomeView(view: ViewPager2) { init { view.setId(View.generateViewId()) } } """ ) .indented(), ) .skipTestModes(FULLY_QUALIFIED, IMPORT_ALIAS) // TODO relies on non-qualified matching. .run() .expect( """ src/foo/SomeView.kt:8: Error: Use an id defined in resources or a statically created instead of generating with View.generateViewId(). See https://issuetracker.google.com/issues/185820237 [DenyListedApi] view.setId(View.generateViewId()) ~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun errorLinkedList() { lint() .files( kotlin( """ package foo import java.util.LinkedList class SomeClass { val stuff = LinkedList() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: For a stack/queue/double-ended queue use ArrayDeque, for a list use ArrayList. Both are more efficient internally. [DenyListedApi] val stuff = LinkedList() ~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun errorStack() { lint() .files( kotlin( """ package foo import java.util.Stack class SomeClass { val stuff = Stack() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: For a stack use ArrayDeque which is more efficient internally. [DenyListedApi] val stuff = Stack() ~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun errorVector() { lint() .files( kotlin( """ package foo import java.util.Vector class SomeClass { val stuff = Vector() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: For a vector use ArrayList or ArrayDeque which are more efficient internally. [DenyListedApi] val stuff = Vector() ~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun schedulersNewThread() { lint() .files( SCHEDULERS_STUB, kotlin( """ package foo import io.reactivex.rxjava3.schedulers.Schedulers class SomeClass { val scheduler = Schedulers.newThread() } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use a scheduler which wraps a cached set of threads. There should be no reason to be arbitrarily creating threads on Android. [DenyListedApi] val scheduler = Schedulers.newThread() ~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Ignore("Revisiting after we look more into how this would conflict with MagicNumber") @Test fun buildVersionCodes() { lint() .files( kotlin( """ package foo import android.os.Build import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.S class SomeClass { val p = android.os.Build.VERSION_CODES.P val q = Build.VERSION_CODES.Q val r = VERSION_CODES.R val s = S } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:5: Error: No one remembers what these constants map to. Use the API level integer value directly since it's self-defining. [DenyListedApi] import android.os.Build.VERSION_CODES.S ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/SomeClass.kt:8: Error: No one remembers what these constants map to. Use the API level integer value directly since it's self-defining. [DenyListedApi] val p = android.os.Build.VERSION_CODES.P ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/foo/SomeClass.kt:9: Error: No one remembers what these constants map to. Use the API level integer value directly since it's self-defining. [DenyListedApi] val q = Build.VERSION_CODES.Q ~~~~~~~~~~~~~~~~~~~~~ src/foo/SomeClass.kt:10: Error: No one remembers what these constants map to. Use the API level integer value directly since it's self-defining. [DenyListedApi] val r = VERSION_CODES.R ~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Ignore("Not enabled currently") @Test fun javaTimeInstantNow() { lint() .files( kotlin( """ package foo import java.time.Instant class SomeClass { val now = Instant.now() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use com.squareup.cash.util.Clock to get the time. [DenyListedApi] val now = Instant.now() ~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaUtilDate() { lint() .files( kotlin( """ package foo import java.util.Date class SomeClass { val now = Date() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.Instant or java.time.ZonedDateTime instead. There is no reason to use java.util.Date in Java 8+. [DenyListedApi] val now = Date() ~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTextDateFormatField() { lint() .files( kotlin( """ package foo import java.text.DateFormat class SomeClass { val yearField = DateFormat.YEAR_FIELD } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+. [DenyListedApi] val yearField = DateFormat.YEAR_FIELD ~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTextDateFormatFunction() { lint() .files( kotlin( """ package foo import java.text.DateFormat class SomeClass { val dateFormat = DateFormat.getDateInstance() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+. [DenyListedApi] val dateFormat = DateFormat.getDateInstance() ~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTextSimpleDateFormatField() { lint() .files( kotlin( """ package foo import java.text.SimpleDateFormat class SomeClass { val yearField = SimpleDateFormat.YEAR_FIELD } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+. [DenyListedApi] val yearField = SimpleDateFormat.YEAR_FIELD ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTextSimpleDateFormatFunction() { lint() .files( kotlin( """ package foo import java.text.SimpleDateFormat class SomeClass { val dateFormat = SimpleDateFormat.getDateInstance() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.DateTimeFormatter instead. There is no reason to use java.text.DateFormat in Java 8+. [DenyListedApi] val dateFormat = SimpleDateFormat.getDateInstance() ~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaUtilCalendarField() { lint() .files( kotlin( """ package foo import java.util.Calendar class SomeClass { val hourOfDay = Calendar.HOUR_OF_DAY } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.Instant or java.time.ZonedDateTime instead. There is no reason to use java.util.Calendar in Java 8+. [DenyListedApi] val hourOfDay = Calendar.HOUR_OF_DAY ~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun javaUtilCalendarFunction() { lint() .files( kotlin( """ package foo import java.util.Calendar class SomeClass { val calendar = Calendar.getInstance() } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Use java.time.Instant or java.time.ZonedDateTime instead. There is no reason to use java.util.Calendar in Java 8+. [DenyListedApi] val calendar = Calendar.getInstance() ~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun rxCompletableParameterless() { lint() .files( COROUTINE_SCOPE_STUB, COMPLETABLE_STUB, RX_COMPLETABLE_STUB, kotlin( """ package foo import kotlinx.coroutines.rx3.rxCompletable class SomeClass { val now = rxCompletable {} } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: rxCompletable defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic. [DenyListedApi] val now = rxCompletable {} ~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun rxSingleParameterless() { lint() .files( COROUTINE_SCOPE_STUB, SINGLE_STUB, RX_SINGLE_STUB, kotlin( """ package foo import kotlinx.coroutines.rx3.rxSingle class SomeClass { val now = rxSingle { "a" } } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: rxSingle defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic. [DenyListedApi] val now = rxSingle { "a" } ~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun rxMaybeParameterless() { lint() .files( COROUTINE_SCOPE_STUB, MAYBE_STUB, RX_MAYBE_STUB, kotlin( """ package foo import kotlinx.coroutines.rx3.rxMaybe class SomeClass { val now = rxMaybe { "a" } } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: rxMaybe defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic. [DenyListedApi] val now = rxMaybe { "a" } ~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun rxObservableParameterless() { lint() .files( COROUTINE_SCOPE_STUB, TEST_OBSERVER_STUB, OBSERVABLE_STUB, PRODUCER_STUB, RX_OBSERVABLE_STUB, kotlin( """ package foo import kotlinx.coroutines.rx3.rxObservable class SomeClass { val now = rxObservable { send("a") } } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: rxObservable defaults to Dispatchers.Default. Provide an explicit dispatcher which can be replaced with a test dispatcher to make your tests more deterministic. [DenyListedApi] val now = rxObservable { send("a") } ~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun rxCompletableWithParameters() { lint() .files( COROUTINE_SCOPE_STUB, COMPLETABLE_STUB, RX_COMPLETABLE_STUB, kotlin( """ package foo import kotlinx.coroutines.rx3.rxCompletable object MyDispatcher : CoroutineContext class SomeClass { val now = rxCompletable(MyDispatcher) {} } """ ) .indented(), ) .run() .expectClean() } @Test fun runCatching() { lint() .files( kotlin( """ package foo class SomeClass { val result = runCatching {} } """ ) .indented() ) .run() .expect( """ src/foo/SomeClass.kt:4: Error: runCatching has hidden issues when used with coroutines as it catches and doesn't rethrow CancellationException. This can interfere with coroutines cancellation handling! Prefer catching specific exceptions based on the current case. [DenyListedApi] val result = runCatching {} ~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun coroutineRunBlocking() { lint() .files( RUN_BLOCKING_STUB, kotlin( """ package foo import kotlinx.coroutines.runBlocking class SomeClass { val result = runBlocking {} } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:6: Error: Blocking calls in coroutines can cause deadlocks and application jank. Prefer making the enclosing function a suspend function or refactoring this in a way to use non-blocking calls. If running in a test, use runTest {} or Turbine to test synchronous values. [DenyListedBlockingApi] val result = runBlocking {} ~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) } @Test fun rxJavaBlocking() { lint() .files( COMPLETABLE_STUB, SINGLE_STUB, MAYBE_STUB, OBSERVABLE_STUB, TEST_OBSERVER_STUB, FLOWABLE_STUB, TEST_SUBSCRIBER_STUB, kotlin( """ package foo import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Flowable class SomeClass { val singleCase = Single.never().blockingGet() val singleTest = Single.never().test() val maybeCase = Maybe.never().blockingGet() val maybeTest = Maybe.never().test() val observableCase = Observable.never().blockingFirst() val observableTest = Observable.never().test() val flowableCase = Flowable.never().blockingFirst() val flowableTest = Flowable.never().test() fun test() { Completable.never().blockingAwait() Completable.never().test() } } """ ) .indented(), ) .run() .expect( """ src/foo/SomeClass.kt:10: Error: Blocking calls in RxJava can cause deadlocks and application jank. Prefer making the enclosing method/function return this Single, a Disposable to grant control to the caller, Completable (if you want to hide emission values but defer subscription), or refactoring this in a way to use non-blocking calls. If running in a test, use the .test()/TestObserver API (https://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/observers/TestObserver.html) test synchronous values. [DenyListedBlockingApi] val singleCase = Single.never().blockingGet() ~~~~~~~~~~~ src/foo/SomeClass.kt:12: Error: Blocking calls in RxJava can cause deadlocks and application jank. Prefer making the enclosing method/function return this Maybe, a Disposable to grant control to the caller, Completable (if you want to hide emission values but defer subscription), or refactoring this in a way to use non-blocking calls. If running in a test, use the .test()/TestObserver API (https://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/observers/TestObserver.html) test synchronous values. [DenyListedBlockingApi] val maybeCase = Maybe.never().blockingGet() ~~~~~~~~~~~ src/foo/SomeClass.kt:14: Error: Blocking calls in RxJava can cause deadlocks and application jank. Prefer making the enclosing method/function return this Observable, a Disposable to grant control to the caller, Completable (if you want to hide emission values but defer subscription), or refactoring this in a way to use non-blocking calls. If running in a test, use the .test()/TestObserver API (https://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/observers/TestObserver.html) test synchronous values. [DenyListedBlockingApi] val observableCase = Observable.never().blockingFirst() ~~~~~~~~~~~~~ src/foo/SomeClass.kt:16: Error: Blocking calls in RxJava can cause deadlocks and application jank. Prefer making the enclosing method/function return this Flowable, a Disposable to grant control to the caller, Completable (if you want to hide emission values but defer subscription), or refactoring this in a way to use non-blocking calls. If running in a test, use the .test()/TestObserver API (https://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/observers/TestObserver.html) test synchronous values. [DenyListedBlockingApi] val flowableCase = Flowable.never().blockingFirst() ~~~~~~~~~~~~~ src/foo/SomeClass.kt:20: Error: Blocking calls in RxJava can cause deadlocks and application jank. Prefer making the enclosing method/function return this Completable, a Disposable to grant control to the caller, or refactoring this in a way to use non-blocking calls. If running in a test, use the .test()/TestObserver API (https://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/observers/TestObserver.html) test synchronous values. [DenyListedBlockingApi] Completable.never().blockingAwait() ~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) } companion object { private val FLOWABLE_STUB = java( """ package io.reactivex.rxjava3.core; import io.reactivex.rxjava3.subscribers.TestSubscriber; public final class Flowable { public static Flowable just(T item) {} public static Flowable never() { return new Flowable<>(); } public T blockingFirst() { return null; } public TestSubscriber test() {} public TestSubscriber test(boolean dispose) {} } """ ) .indented() private val TEST_SUBSCRIBER_STUB = java( """ package io.reactivex.rxjava3.subscribers; public class TestSubscriber { public final assertValue(T value) {} } """ ) .indented() private val OBSERVABLE_STUB = java( """ package io.reactivex.rxjava3.core; import io.reactivex.rxjava3.observers.TestObserver; public final class Observable { public static Observable just(T item) {} public static Observable never() { return new Observable<>(); } public T blockingFirst() { return null; } public TestObserver test() {} public TestObserver test(boolean dispose) {} } """ ) .indented() private val TEST_OBSERVER_STUB = java( """ package io.reactivex.rxjava3.observers; public final class TestObserver { public assertValue(T value) {} } """ ) .indented() private val RX_RULE_STUB = kotlin( """ package com.squareup.util.rx3.test import io.reactivex.rxjava3.core.Observable class RxRule { fun newObserver(): RecordingObserver = TODO() } fun Observable.test(rxRule: RxRule): RecordingObserver """ ) .indented() private val CONTEXT_COMPAT_STUB = java( """ package androidx.core.content; import android.graphics.drawable.Drawable; import android.content.Context; public class ContextCompat { public static Drawable getDrawable(Context context, int id) {} } """ ) .indented() private val VIEWCOMPAT_STUB = java( """ package androidx.core.view; public class ViewCompat { public static int generateViewId() { return 0; } } """ ) .indented() private val VIEWPAGER2_STUB = java( """ package androidx.viewpager2.widget; public class ViewPager2 { public void setId(int id) {} } """ ) .indented() private val SCHEDULERS_STUB = java( """ package io.reactivex.rxjava3.schedulers; public final class Schedulers { public static Object newThread() { return null; } } """ ) .indented() private val COROUTINE_SCOPE_STUB = kotlin( """ package kotlinx.coroutines interface CoroutineScope """ ) .indented() private val COMPLETABLE_STUB = java( """ package io.reactivex.rxjava3.core; public final class Completable { Completable() {} public static Completable never() { return new Completable(); } public void blockingAwait() { } public TestObserver test() { return new TestObserver<>(); } } """ ) .indented() private val RX_COMPLETABLE_STUB = kotlin( """ package kotlinx.coroutines.rx3 import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope class RxCompletable {} public fun rxCompletable( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit ): Completable { return Completable } """ ) .indented() private val SINGLE_STUB = java( """ package io.reactivex.rxjava3.core; public final class Single { Single() {} public static Single never() { return new Single<>(); } public T blockingGet() { return null; } public TestObserver test() { return new TestObserver<>(); } } """ ) .indented() private val RX_SINGLE_STUB = kotlin( """ package kotlinx.coroutines.rx3 import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope class RxSingle {} public fun rxSingle( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): Single { return Single() } """ ) .indented() private val MAYBE_STUB = java( """ package io.reactivex.rxjava3.core; public final class Maybe { Maybe() {} public static Maybe never() { return new Maybe<>(); } public T blockingGet() { return null; } public TestObserver test() { return new TestObserver<>(); } } """ ) .indented() private val RX_MAYBE_STUB = kotlin( """ package kotlinx.coroutines.rx3 import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope class rxMaybe {} public fun rxMaybe( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T? ): Maybe { return Maybe() } """ ) .indented() private val PRODUCER_STUB = kotlin( """ package kotlinx.coroutines.channels object ProducerScope { suspend fun send(value: T) } """ ) .indented() private val RUN_BLOCKING_STUB = kotlin( """ @file:JvmName("BuildersKt") package kotlinx.coroutines fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { TODO() } """ ) .indented() private val RX_OBSERVABLE_STUB = kotlin( """ package kotlinx.coroutines.rx3 import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.ProducerScope class RxObservable {} public fun rxObservable( context: CoroutineContext = EmptyCoroutineContext, block: suspend ProducerScope.() -> Unit ): Observable { return Observable() } """ ) .indented() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/eithernet/DoNotExposeEitherNetInRepositoriesDetectorTest.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.eithernet import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test import slack.lint.BaseSlackLintTest private val API_RESULT = kotlin( """ package com.slack.eithernet interface ApiResult """ ) .indented() class DoNotExposeEitherNetInRepositoriesDetectorTest : BaseSlackLintTest() { override fun getDetector() = DoNotExposeEitherNetInRepositoriesDetector() override fun getIssues() = listOf(DoNotExposeEitherNetInRepositoriesDetector.ISSUE) // TODO fix these override val skipTestModes: Array = arrayOf(TestMode.SUPPRESSIBLE) @Test fun javaTests() { lint() .files( API_RESULT, java( """ package test; import com.slack.eithernet.ApiResult; interface MyRepository { // Bad ApiResult getResult(); // Good String getString(); } """ ) .indented(), // Non-interface version java( """ package test; import com.slack.eithernet.ApiResult; abstract class MyClassRepository { // Bad public abstract ApiResult getResultPublic(); public ApiResult resultField = null; // Good ApiResult resultFieldPackagePrivate = null; private final ApiResult resultFieldPrivate = null; protected ApiResult resultFieldProtected = null; abstract ApiResult getResultPackagePrivate(); private ApiResult getResultPrivate(); private ApiResult getResultProtected(); public abstract String getString(); } """ ) .indented(), ) .run() .expect( """ src/test/MyClassRepository.java:8: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] public abstract ApiResult getResultPublic(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/MyClassRepository.java:9: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] public ApiResult resultField = null; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/MyRepository.java:8: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] ApiResult getResult(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) } @Test fun kotlinTests() { lint() .files( API_RESULT, kotlin( """ package test import com.slack.eithernet.ApiResult interface MyRepository { // Bad fun getResult(): ApiResult suspend fun getResultSuspended(): ApiResult val resultVal: ApiResult // Good fun getString(): String suspend fun getStringSuspended(): String val stringValue: String } """ ) .indented(), // Non-interface version kotlin( """ package test import com.slack.eithernet.ApiResult abstract class MyClassRepository { // Bad abstract fun getResultPublic(): ApiResult fun typeLessFunction() = getResultPublic() val resultProperty: ApiResult? = null val typeLessProperty get() = resultProperty // Good internal val resultPropertyInternal: ApiResult? = null private val resultPropertyPrivate: ApiResult? = null protected val resultPropertyProtected: ApiResult? = null internal abstract fun getResultInternal(): ApiResult private fun getResultPrivate(): ApiResult private fun getResultProtected(): ApiResult abstract fun getString(): String } """ ) .indented(), ) .run() .expect( """ src/test/MyClassRepository.kt:8: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] abstract fun getResultPublic(): ApiResult ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/MyClassRepository.kt:9: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] fun typeLessFunction() = getResultPublic() ~~~~~~~~~~~~~~~~ src/test/MyClassRepository.kt:10: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] val resultProperty: ApiResult? = null ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/MyClassRepository.kt:11: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] val typeLessProperty get() = resultProperty ~~~~~~~~~~~~~~~~ src/test/MyRepository.kt:8: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] fun getResult(): ApiResult ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/MyRepository.kt:9: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] suspend fun getResultSuspended(): ApiResult ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } @Test fun regressionTest() { lint() .files( API_RESULT, kotlin( """ package test import com.slack.eithernet.ApiResult interface StuffRepository { suspend fun fetchStuff(): ApiResult suspend fun setStuff( discoverability: String ): ApiResult } """ ) .indented(), ) .run() .expect( """ src/test/StuffRepository.kt:7: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] suspend fun fetchStuff(): ApiResult ~~~~~~~~~~~~~~~~~~~~~~~~~ src/test/StuffRepository.kt:11: Error: Repository APIs should not expose EitherNet types directly. [DoNotExposeEitherNetInRepositories] ): ApiResult ~~~~~~~~~~~~~~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/inclusive/InclusiveNamingDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.inclusive import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.checks.infrastructure.TestMode import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Ignore import org.junit.Test import slack.lint.BaseSlackLintTest class InclusiveNamingDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = InclusiveNamingSourceCodeScanner() override fun getIssues(): List = InclusiveNamingChecker.ISSUES.toList() override fun lint(): TestLintTask { return super.lint().configureOption(InclusiveNamingChecker.BLOCK_LIST, "fork,knife,spoon,spork") } override val skipTestModes: Array = arrayOf( // TODO fix these TestMode.SUPPRESSIBLE, // Aliases are impossible to test correctly because you have to maintain completely different // expected fixes and source inputs TestMode.TYPE_ALIAS, ) @Test fun kotlin() { // This covers the following cases: // - Class // - Parameter // - Property // - Local var // - Label // - Function lint() .files( kotlin( "test/ForkHandler.kt", """ class Spork(val sporkBranch: String, sporkParam: String) { val knife = "" fun spoonBranch(val spoonRef: String) { val localFork = "" emptyList() .forEach spoonRefs@ { } } } """, ) .indented() ) .run() .expect( """ test/ForkHandler.kt:1: Error: Use inclusive naming. Matched string is 'fork' in file name 'ForkHandler.kt' [InclusiveNaming] class Spork(val sporkBranch: String, sporkParam: String) { ^ test/ForkHandler.kt:1: Error: Use inclusive naming. Matched string is 'spork' in class name 'Spork' [InclusiveNaming] class Spork(val sporkBranch: String, sporkParam: String) { ^ test/ForkHandler.kt:1: Error: Use inclusive naming. Matched string is 'spork' in function name 'getSporkBranch' [InclusiveNaming] class Spork(val sporkBranch: String, sporkParam: String) { ~~~~~~~~~~~ test/ForkHandler.kt:1: Error: Use inclusive naming. Matched string is 'spork' in parameter name 'sporkParam' [InclusiveNaming] class Spork(val sporkBranch: String, sporkParam: String) { ~~~~~~~~~~~~~~~~~~ test/ForkHandler.kt:1: Error: Use inclusive naming. Matched string is 'spork' in property name 'sporkBranch' [InclusiveNaming] class Spork(val sporkBranch: String, sporkParam: String) { ~~~~~~~~~~~~~~~~~~~~~~~ test/ForkHandler.kt:2: Error: Use inclusive naming. Matched string is 'knife' in function name 'getKnife' [InclusiveNaming] val knife = "" ~~~~~ test/ForkHandler.kt:2: Error: Use inclusive naming. Matched string is 'knife' in property name 'knife' [InclusiveNaming] val knife = "" ~~~~~~~~~~~~~~ test/ForkHandler.kt:4: Error: Use inclusive naming. Matched string is 'spoon' in function name 'spoonBranch' [InclusiveNaming] fun spoonBranch(val spoonRef: String) { ~~~~~~~~~~~ test/ForkHandler.kt:4: Error: Use inclusive naming. Matched string is 'spoon' in parameter name 'spoonRef' [InclusiveNaming] fun spoonBranch(val spoonRef: String) { ~~~~~~~~~~~~~~~~~~~~ test/ForkHandler.kt:5: Error: Use inclusive naming. Matched string is 'fork' in local variable name 'localFork' [InclusiveNaming] val localFork = "" ~~~~~~~~~~~~~~~~~~ test/ForkHandler.kt:7: Error: Use inclusive naming. Matched string is 'spoon' in label name 'spoonRefs' [InclusiveNaming] .forEach spoonRefs@ { ^ 11 errors, 0 warnings """ .trimIndent() ) } @Test fun java() { // This covers the following cases: // - Class // - Parameter // - Field // - Local var // - Method lint() .files( java( "test/ForkHandler.java", """ class ForkHandler { String knife = ""; void spoonBranch(String spoonRef) { String localFork = ""; } } """, ) .indented() ) .run() .expect( """ test/ForkHandler.java:1: Error: Use inclusive naming. Matched string is 'fork' in class name 'ForkHandler' [InclusiveNaming] class ForkHandler { ^ test/ForkHandler.java:1: Error: Use inclusive naming. Matched string is 'fork' in file name 'ForkHandler.java' [InclusiveNaming] class ForkHandler { ^ test/ForkHandler.java:2: Error: Use inclusive naming. Matched string is 'knife' in field name 'knife' [InclusiveNaming] String knife = ""; ~~~~~~~~~~~~~~~~~~ test/ForkHandler.java:4: Error: Use inclusive naming. Matched string is 'spoon' in method name 'spoonBranch' [InclusiveNaming] void spoonBranch(String spoonRef) { ~~~~~~~~~~~ test/ForkHandler.java:4: Error: Use inclusive naming. Matched string is 'spoon' in parameter name 'spoonRef' [InclusiveNaming] void spoonBranch(String spoonRef) { ~~~~~~~~~~~~~~~ test/ForkHandler.java:5: Error: Use inclusive naming. Matched string is 'fork' in local variable name 'localFork' [InclusiveNaming] String localFork = ""; ~~~~~~~~~~~~~~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } @Ignore("Not working juuuuuust yet. Left as a toe-hold") @Test fun xml() { // Attr lint() .files( xml( "test_file.xml", """ """, ) .indented() ) .run() .expect( """ """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/AutoValueMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Test import slack.lint.BaseSlackLintTest class AutoValueMockDetectorTest : BaseSlackLintTest() { private val autoValueAnnotationClass = kotlin( """ package com.google.auto.value annotation class AutoValue { annotation class Builder } """ ) private val testClass = java( """ package slack.test; import com.google.auto.value.AutoValue; @AutoValue public abstract class TestClass { public static Builder builder() { return null; } @AutoValue.Builder public abstract class Builder { abstract TestClass build(); } } """ ) .indented() override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass @Mock lateinit var fieldBuilderMock: TestClass.Builder @Spy lateinit var fieldBuilderSpy: TestClass.Builder fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val builderLocalMock1 = org.mockito.Mockito.mock(TestClass.Builder::class.java) val builderLocalSpy1 = org.mockito.Mockito.spy(builderLocalMock1) val builderLocalMock2 = mock() val builderClassRef = TestClass.Builder::class.java val builderLocalMock3 = org.mockito.Mockito.mock(classRef) val fake = TestClass.builder().build() } } """, ) .indented() lint() .files(*mockFileStubs(), autoValueAnnotationClass, testClass, source) .allowCompilationErrors() // Until AGP 7.1.0 // https://groups.google.com/g/lint-dev/c/BigCO8sMhKU .run() .expect( """ test/test/slack/test/TestClass.kt:8: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] @Mock lateinit var fieldMock: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] @Spy lateinit var fieldSpy: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:10: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] @Mock lateinit var fieldBuilderMock: TestClass.Builder ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:11: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] @Spy lateinit var fieldBuilderSpy: TestClass.Builder ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:14: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:15: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] val localMock2 = mock() ~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:20: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] val builderLocalMock1 = org.mockito.Mockito.mock(TestClass.Builder::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:21: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] val builderLocalSpy1 = org.mockito.Mockito.spy(builderLocalMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:22: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] val builderLocalMock2 = mock() ~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:24: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] val builderLocalMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; @Mock TestClass.Builder fieldMock; @Spy TestClass.Builder fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass.Builder builderLocalMock = mock(TestClass.Builder.class); TestClass.Builder builderLocalSpy = spy(builderLocalMock); Class builderClassRef = TestClass.Builder.class; TestClass.Builder builderLocalMock2 = mock(builderClassRef); TestClass fake = TestClass.builder().build(); } } """, ) .indented() lint() .files(*mockFileStubs(), autoValueAnnotationClass, testClass, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] @Mock TestClass fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] @Spy TestClass fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:11: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] @Mock TestClass.Builder fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:12: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] @Spy TestClass.Builder fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:15: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] TestClass localMock = mock(TestClass.class); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] TestClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:18: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue [DoNotMockAutoValue] TestClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:20: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] TestClass.Builder builderLocalMock = mock(TestClass.Builder.class); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:21: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] TestClass.Builder builderLocalSpy = spy(builderLocalMock); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:23: Error: Mocked type is annotated with non-mockable annotation com.google.auto.value.AutoValue.Builder [DoNotMockAutoValue] TestClass.Builder builderLocalMock2 = mock(builderClassRef); ~~~~~~~~~~~~~~~~~~~~~ 10 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/DataClassMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Test import slack.lint.BaseSlackLintTest class DataClassMockDetectorTest : BaseSlackLintTest() { private val testClass = kotlin( """ package slack.test data class TestClass(val foo: String, val list: List = emptyList()) """ ) .indented() override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val assigned: TestClass = mock() val fake = TestClass("this is fine") // Extra tests for location reporting val unnecessaryMockedValues = TestClass( "This is fine", mock() ) val unnecessaryNestedMockedValues = TestClass( "This is fine", listOf(mock()) ) val withNamedArgs = TestClass( foo = "This is fine", list = listOf(mock()) ) } } """, ) .indented() lint() .files(*mockFileStubs(), testClass, source) .run() .expect( """ test/test/slack/test/TestClass.kt:8: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] @Mock lateinit var fieldMock: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] @Spy lateinit var fieldSpy: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:12: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:13: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:14: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localMock2 = mock() ~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val dynamicMock = mock { ^ test/test/slack/test/TestClass.kt:21: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val assigned: TestClass = mock() ~~~~~~ test/test/slack/test/TestClass.kt:31: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] listOf(mock()) ~~~~~~ test/test/slack/test/TestClass.kt:35: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] list = listOf(mock()) ~~~~~~ test/test/slack/test/TestClass.kt:27: Error: platform type 'java.util.List' should not be mocked [DoNotMockPlatformTypes] mock() ~~~~~~ 11 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass fake = new TestClass("this is fine"); } } """, ) .indented() lint() .files(*mockFileStubs(), testClass, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] @Mock TestClass fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] @Spy TestClass fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:13: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] TestClass localMock = mock(TestClass.class); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:14: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] TestClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: 'slack.test.TestClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] TestClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/DoNotMockMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Test import slack.lint.BaseSlackLintTest class DoNotMockMockDetectorTest : BaseSlackLintTest() { private val slackDoNotMock = kotlin( """ package slack.lint.annotations annotation class DoNotMock(val value: String = "BECAUSE REASONS") """ ) .indented() private val epDoNotMock = kotlin( """ package com.google.errorprone.annotations annotation class DoNotMock(val value: String = "BECAUSE REASONS") """ ) .indented() private val testClass = kotlin( """ package slack.test @slack.lint.annotations.DoNotMock("Use fake()") interface TestClass { fun fake(): TestClass? = null } @com.google.errorprone.annotations.DoNotMock("Use fake()") interface TestClass2 { fun fake(): TestClass2? = null } @slack.lint.annotations.DoNotMock interface TestClass3 { fun fake(): TestClass3? = null } @com.google.errorprone.annotations.DoNotMock interface TestClass4 { fun fake(): TestClass4? = null } """ ) .indented() override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock class MyTests { @Mock lateinit var mock1: TestClass @Mock lateinit var mock2: TestClass2 @Mock lateinit var mock3: TestClass3 @Mock lateinit var mock4: TestClass4 } """, ) .indented() lint() .files(*mockFileStubs(), slackDoNotMock, epDoNotMock, testClass, source) .run() .expect( """ test/test/slack/test/TestClass.kt:6: Error: Do not mock TestClass: Use fake() [DoNotMock] @Mock lateinit var mock1: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:7: Error: Do not mock TestClass2: Use fake() [DoNotMock] @Mock lateinit var mock2: TestClass2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:8: Error: Do not mock TestClass3: BECAUSE REASONS [DoNotMock] @Mock lateinit var mock3: TestClass3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: Do not mock TestClass4: BECAUSE REASONS [DoNotMock] @Mock lateinit var mock4: TestClass4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass mock; @Mock TestClass2 mock2; @Mock TestClass3 mock3; @Mock TestClass4 mock4; public void example() { } } """, ) .indented() lint() .files(*mockFileStubs(), slackDoNotMock, epDoNotMock, testClass, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: Do not mock TestClass: Use fake() [DoNotMock] @Mock TestClass mock; ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: Do not mock TestClass2: Use fake() [DoNotMock] @Mock TestClass2 mock2; ~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:11: Error: Do not mock TestClass3: BECAUSE REASONS [DoNotMock] @Mock TestClass3 mock3; ~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:12: Error: Do not mock TestClass4: BECAUSE REASONS [DoNotMock] @Mock TestClass4 mock4; ~~~~~~~~~~~~~~~~~~~~~~~ 4 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/DoNotMockUsageDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test import slack.lint.BaseSlackLintTest class DoNotMockUsageDetectorTest : BaseSlackLintTest() { // TODO fix these override val skipTestModes: Array = arrayOf(TestMode.TYPE_ALIAS, TestMode.IMPORT_ALIAS) private val slackDoNotMock = kotlin( """ package slack.lint.annotations annotation class DoNotMock(val value: String = "BECAUSE REASONS") """ ) .indented() private val epDoNotMock = kotlin( """ package com.google.errorprone.annotations annotation class DoNotMock(val value: String = "BECAUSE REASONS") """ ) .indented() override fun getDetector() = ErrorProneDoNotMockDetector() override fun getIssues() = listOf(ErrorProneDoNotMockDetector.ISSUE) @Test fun kotlinTests() { val source = kotlin( """ package slack.test import com.google.errorprone.annotations.DoNotMock @slack.lint.annotations.DoNotMock("Use fake()") interface TestClass { fun fake(): TestClass? = null } @com.google.errorprone.annotations.DoNotMock("Use fake()") interface TestClass2 { fun fake(): TestClass2? = null } @slack.lint.annotations.DoNotMock interface TestClass3 { fun fake(): TestClass3? = null } @DoNotMock interface TestClass4 { fun fake(): TestClass4? = null } """ ) .indented() lint() .files(*mockFileStubs(), slackDoNotMock, epDoNotMock, source) .run() .expect( """ src/slack/test/TestClass.kt:10: Error: Use Slack's internal @DoNotMock annotation. [ErrorProneDoNotMockUsage] @com.google.errorprone.annotations.DoNotMock("Use fake()") ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/test/TestClass.kt:20: Error: Use Slack's internal @DoNotMock annotation. [ErrorProneDoNotMockUsage] @DoNotMock ~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/test/TestClass.kt line 10: Replace with slack.lint.annotations.DoNotMock: @@ -10 +10 - @com.google.errorprone.annotations.DoNotMock("Use fake()") + @slack.lint.annotations.DoNotMock("Use fake()") Fix for src/slack/test/TestClass.kt line 20: Replace with slack.lint.annotations.DoNotMock: @@ -20 +20 - @DoNotMock + @slack.lint.annotations.DoNotMock """ .trimIndent() ) } @Test fun javaTests() { val source = java( """ package slack.test; import com.google.errorprone.annotations.DoNotMock; @slack.lint.annotations.DoNotMock("Use fake()") interface TestClass { } @com.google.errorprone.annotations.DoNotMock("Use fake()") interface TestClass2 { } @slack.lint.annotations.DoNotMock interface TestClass3 { } @DoNotMock interface TestClass4 { } """ ) .indented() lint() .files(*mockFileStubs(), slackDoNotMock, epDoNotMock, source) .run() .expect( """ src/slack/test/TestClass.java:9: Error: Use Slack's internal @DoNotMock annotation. [ErrorProneDoNotMockUsage] @com.google.errorprone.annotations.DoNotMock("Use fake()") ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/slack/test/TestClass.java:17: Error: Use Slack's internal @DoNotMock annotation. [ErrorProneDoNotMockUsage] @DoNotMock ~~~~~~~~~~ 2 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/test/TestClass.java line 9: Replace with slack.lint.annotations.DoNotMock: @@ -9 +9 - @com.google.errorprone.annotations.DoNotMock("Use fake()") + @slack.lint.annotations.DoNotMock("Use fake()") Fix for src/slack/test/TestClass.java line 17: Replace with slack.lint.annotations.DoNotMock: @@ -17 +17 - @DoNotMock + @slack.lint.annotations.DoNotMock """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/MockDetectorOptionsTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.checks.infrastructure.TestLintTask import org.junit.Test import slack.lint.BaseSlackLintTest class MockDetectorOptionsTest : BaseSlackLintTest() { override fun lint(): TestLintTask { return super.lint() .configureOption( MockDetector.MOCK_ANNOTATIONS, "io.mockk.impl.annotations.MockK,io.mockk.impl.annotations.SpyK", ) .configureOption( MockDetector.MOCK_FACTORIES, "io.mockk.MockKKt#mockk,io.mockk.MockKKt#mockkClass,io.mockk.MockKKt#mockkObject,io.mockk.MockKKt#spyk", ) } override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun tests() { val source = kotlin( "test/slack/test/MyTests.kt", """ package slack.test import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.mockk import io.mockk.mockkClass import io.mockk.mockkObject import io.mockk.spyk class MyTests { @MockK lateinit var fieldDataMock: DataClass @SpyK lateinit var fieldDataSpy: DataClass fun dataClass() { val localMock1: DataClass = mockk() val localMock2 = mockk() val localSpy1: DataClass = spyk() val localSpy2 = spyk() val localSpy3 = spyk(localMock1) // KClass is not detected, explicit type needed! val localClassMock = mockkClass(DataClass::class) } @MockK lateinit var fieldSealedMock: SealedClass @SpyK lateinit var fieldSealedSpy: SealedClass fun sealedClass() { val localMock1: SealedClass = mockk() val localMock2 = mockk() val localSpy1: SealedClass = spyk() val localSpy2 = spyk() val localSpy3 = spyk(localMock1) // KClass is not detected, explicit type needed! val localClassMock = mockkClass(SealedClass::class) } @MockK lateinit var fieldObjectMock: ObjectClass @SpyK lateinit var fieldObjectSpy: ObjectClass fun objectClass() { val localMock1: ObjectClass = mockk() val localMock2 = mockk() val localSpy1: ObjectClass = spyk() val localSpy2 = spyk() val localSpy3 = spyk(localMock1) // KClass is not detected, explicit type needed! val localClassMock = mockkClass(ObjectClass::class) // Wrong Error: 'kotlin.Unit' is an object, so mocking it should not be necessary [DoNotMockObjectClass] // Wrong Warning: platform type 'kotlin.Unit' should not be mocked [DoNotMockPlatformTypes] // mockkObject(ObjectClass) } fun platformTypes() { // java. mockk>() mockk() // kotlin. mockk() mockk>() } } """, ) .indented() lint() .files(*stubs(), source) .run() .expect( """ test/slack/test/MyTests.kt:11: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] @MockK lateinit var fieldDataMock: DataClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:12: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] @SpyK lateinit var fieldDataSpy: DataClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:15: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localMock1: DataClass = mockk() ~~~~~~~ test/slack/test/MyTests.kt:16: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localMock2 = mockk() ~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:18: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localSpy1: DataClass = spyk() ~~~~~~ test/slack/test/MyTests.kt:19: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localSpy2 = spyk() ~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:20: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localSpy3 = spyk(localMock1) ~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:23: Error: 'slack.test.DataClass' is a data class, so mocking it should not be necessary [DoNotMockDataClass] val localClassMock = mockkClass(DataClass::class) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:41: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] @MockK lateinit var fieldObjectMock: ObjectClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:42: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] @SpyK lateinit var fieldObjectSpy: ObjectClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:45: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localMock1: ObjectClass = mockk() ~~~~~~~ test/slack/test/MyTests.kt:46: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localMock2 = mockk() ~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:48: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localSpy1: ObjectClass = spyk() ~~~~~~ test/slack/test/MyTests.kt:49: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localSpy2 = spyk() ~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:50: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localSpy3 = spyk(localMock1) ~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:53: Error: 'slack.test.ObjectClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localClassMock = mockkClass(ObjectClass::class) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:62: Error: platform type 'java.lang.Comparable' should not be mocked [DoNotMockPlatformTypes] mockk>() ~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:63: Error: platform type 'java.lang.Runnable' should not be mocked [DoNotMockPlatformTypes] mockk() ~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:65: Error: platform type 'kotlin.io.FileTreeWalk' should not be mocked [DoNotMockPlatformTypes] mockk() ~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:66: Error: platform type 'kotlin.Lazy' should not be mocked [DoNotMockPlatformTypes] mockk>() ~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:26: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @MockK lateinit var fieldSealedMock: SealedClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:27: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @SpyK lateinit var fieldSealedSpy: SealedClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:30: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock1: SealedClass = mockk() ~~~~~~~ test/slack/test/MyTests.kt:31: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock2 = mockk() ~~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:33: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localSpy1: SealedClass = spyk() ~~~~~~ test/slack/test/MyTests.kt:34: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localSpy2 = spyk() ~~~~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:35: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localSpy3 = spyk(localMock1) ~~~~~~~~~~~~~~~~ test/slack/test/MyTests.kt:38: Error: 'slack.test.SealedClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localClassMock = mockkClass(SealedClass::class) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 errors, 0 warnings """ .trimIndent() ) } private val mockK = kotlin( "test/io/mockk/impl/annotations/MockK.kt", """ package io.mockk.impl.annotations annotation class MockK """, ) .indented() private val spyK = kotlin( "test/io/mockk/impl/annotations/SpyK.kt", """ package io.mockk.impl.annotations annotation class SpyK """, ) .indented() private val mockKExtensions = kotlin( "test/io/mockk/MockK.kt", """ package io.mockk inline fun mockk( name: String? = null, relaxed: Boolean = false, vararg moreInterfaces: KClass<*>, relaxUnitFun: Boolean = false, block: T.() -> Unit = {} ): T = TODO() inline fun spyk( name: String? = null, vararg moreInterfaces: KClass<*>, recordPrivateCalls: Boolean = false, block: T.() -> Unit = {} ): T = TODO() inline fun spyk( objToCopy: T, name: String? = null, vararg moreInterfaces: KClass<*>, recordPrivateCalls: Boolean = false, block: T.() -> Unit = {} ): T = TODO() inline fun mockkClass( type: KClass, name: String? = null, relaxed: Boolean = false, vararg moreInterfaces: KClass<*>, relaxUnitFun: Boolean = false, block: T.() -> Unit = {} ): T = TODO() inline fun mockkObject(vararg objects: Any, recordPrivateCalls: Boolean = false): Unit = TODO() """, ) .indented() private val dataClass = kotlin( """ package slack.test data class DataClass(val foo: String, val list: List = emptyList()) """ ) .indented() private val sealedClass = kotlin( """ package slack.test sealed class SealedClass """ ) .indented() private val objectClass = kotlin( """ package slack.test object ObjectClass """ ) .indented() private fun stubs() = arrayOf(mockK, spyK, mockKExtensions, dataClass, sealedClass, objectClass) } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/MockFileStubs.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.checks.infrastructure.TestFiles.java import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin private val mockito = java( """ package org.mockito; public final class Mockito { public static T mock(Class clazz) { return null; } public static T spy(T instance) { return null; } } """ ) .indented() private val mock = java( """ package org.mockito; public @interface Mock { } """ ) .indented() private val spy = java( """ package org.mockito; public @interface Spy { } """ ) .indented() private val mockitoHelpers = kotlin( "test/slack/test/mockito/MockitoHelpers.kt", """ package slack.test.mockito import org.mockito.Mockito inline fun mock(): T = Mockito.mock(T::class.java) inline fun mock(block: T.() -> Unit): T { return Mockito.mock(T::class.java).apply(block) } """, ) .indented() internal fun mockFileStubs() = arrayOf(mockito, mock, spy, mockitoHelpers) ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/MockReportTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.google.common.truth.Truth.assertThat import kotlin.io.path.exists import kotlin.io.path.readText import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import slack.lint.BaseSlackLintTest import slack.lint.mocking.MockDetector.Companion.MOCK_REPORT class MockReportTest : BaseSlackLintTest() { @Rule @JvmField val tmpFolder = TemporaryFolder() private val testClass = kotlin( """ package slack.test data class TestClass(val foo: String, val list: List = emptyList()) """ ) .indented() override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val assigned: TestClass = mock() val fake = TestClass("this is fine") // Extra tests for location reporting val unnecessaryMockedValues = TestClass( "This is fine", mock() ) val unnecessaryNestedMockedValues = TestClass( "This is fine", listOf(mock()) ) val withNamedArgs = TestClass( foo = "This is fine", list = listOf(mock()) ) } } """, ) .indented() val task = lint() .rootDirectory(tmpFolder.root) .files(*mockFileStubs(), testClass, source) .configureOption(MOCK_REPORT, MockDetector.MockReportMode.ERRORS.name) @Suppress("CheckResult") task.run() val reports = tmpFolder.root.toPath().resolve("default/app/${MockDetector.MOCK_REPORT_PATH}") assertThat(reports.exists()).isTrue() assertThat(reports.readText()) .isEqualTo( """ type,isError java.util.List,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass fake = new TestClass("this is fine"); } } """, ) .indented() val task = lint() .rootDirectory(tmpFolder.root) .files(*mockFileStubs(), testClass, source) .configureOption(MOCK_REPORT, MockDetector.MockReportMode.ERRORS.name) @Suppress("CheckResult") task.run() val reports = tmpFolder.root.toPath().resolve("default/app/${MockDetector.MOCK_REPORT_PATH}") assertThat(reports.exists()).isTrue() assertThat(reports.readText()) .isEqualTo( """ type,isError slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true """ .trimIndent() ) } @Test fun allMode() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock import java.lang.Runnable interface ExampleInterface class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass @Mock lateinit var nonErrorMock: ExampleInterface @Spy lateinit var nonErrorSpy: java.lang.ExampleInterface fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val nonErrorMock1 = org.mockito.Mockito.mock(ExampleInterface::class.java) val nonErrorSpy1 = org.mockito.Mockito.spy(nonErrorMock1) val nonErrorMock2 = mock() val classRef = ExampleInterface::class.java val nonErrorMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val dynamicNonErrorMock = mock { } val assigned: TestClass = mock() val assignedNonError: ExampleInterface = mock() val fake = TestClass("this is fine") // Extra tests for location reporting val unnecessaryMockedValues = TestClass( "This is fine", mock() ) val unnecessaryNestedMockedValues = TestClass( "This is fine", listOf(mock()) ) val withNamedArgs = TestClass( foo = "This is fine", list = listOf(mock()) ) } } """, ) .indented() val task = lint() .rootDirectory(tmpFolder.root) .files(*mockFileStubs(), testClass, source) .configureOption(MOCK_REPORT, MockDetector.MockReportMode.ALL.name) @Suppress("CheckResult") task.run() val reports = tmpFolder.root.toPath().resolve("default/app/${MockDetector.MOCK_REPORT_PATH}") assertThat(reports.exists()).isTrue() assertThat(reports.readText()) .isEqualTo( """ type,isError java.util.List,true slack.test.ExampleInterface,false slack.test.ExampleInterface,false slack.test.ExampleInterface,false slack.test.ExampleInterface,false slack.test.ExampleInterface,false slack.test.ExampleInterface,false slack.test.ExampleInterface,false slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true slack.test.TestClass,true """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/ObjectClassMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Test import slack.lint.BaseSlackLintTest class ObjectClassMockDetectorTest : BaseSlackLintTest() { private val testClass = kotlin( """ package slack.test object TestClass """ ) .indented() override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val assigned: TestClass = mock() val fake = TestClass("this is fine") } } """, ) .indented() lint() .files(*mockFileStubs(), testClass, source) .run() .expect( """ test/test/slack/test/TestClass.kt:8: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] @Mock lateinit var fieldMock: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] @Spy lateinit var fieldSpy: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:12: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:13: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:14: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localMock2 = mock() ~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val dynamicMock = mock { ^ test/test/slack/test/TestClass.kt:21: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] val assigned: TestClass = mock() ~~~~~~ 8 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass fake = new TestClass("this is fine"); } } """, ) .indented() lint() .files(*mockFileStubs(), testClass, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] @Mock TestClass fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] @Spy TestClass fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:13: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] TestClass localMock = mock(TestClass.class); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:14: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] TestClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: 'slack.test.TestClass' is an object, so mocking it should not be necessary [DoNotMockObjectClass] TestClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/PlatformTypeMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Test import slack.lint.BaseSlackLintTest class PlatformTypeMockDetectorTest : BaseSlackLintTest() { private val stubs = arrayOf( kotlin( """ package androidx.collection class ArrayMap """ .trimIndent() ) ) override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import slack.test.mockito.mock import java.lang.Runnable import kotlin.io.FileTreeWalk import android.graphics.Typeface import androidx.collection.ArrayMap class MyTests { fun example() { // java. mock>() mock() // kotlin. mock() mock>() // android. mock() // androidx. mock>() } } """, ) .indented() lint() .files(*mockFileStubs(), *stubs, source) .run() .expect( """ test/test/slack/test/TestClass.kt:12: Error: platform type 'java.lang.Comparable' should not be mocked [DoNotMockPlatformTypes] mock>() ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:13: Error: platform type 'java.lang.Runnable' should not be mocked [DoNotMockPlatformTypes] mock() ~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:15: Error: platform type 'kotlin.io.FileTreeWalk' should not be mocked [DoNotMockPlatformTypes] mock() ~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: platform type 'kotlin.Lazy' should not be mocked [DoNotMockPlatformTypes] mock>() ~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: platform type 'android.graphics.Typeface' should not be mocked [DoNotMockPlatformTypes] mock() ~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:20: Error: platform type 'androidx.collection.ArrayMap' should not be mocked [DoNotMockPlatformTypes] mock>() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import static org.mockito.Mockito.mock; import java.lang.Runnable; import kotlin.Lazy; import kotlin.io.FileTreeWalk; import android.graphics.Typeface; import androidx.collection.ArrayMap; class MyTests { public void example() { // java. mock(Comparable.class); mock(Runnable.class); // kotlin. mock(FileTreeWalk.class); mock(Lazy.class); // android. mock(Typeface.class); // androidx. mock(ArrayMap.class); } } """, ) .indented() lint() .files(*mockFileStubs(), *stubs, source) .run() .expect( """ test/test/slack/test/TestClass.java:13: Error: platform type 'java.lang.Comparable' should not be mocked [DoNotMockPlatformTypes] mock(Comparable.class); ~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:14: Error: platform type 'java.lang.Runnable' should not be mocked [DoNotMockPlatformTypes] mock(Runnable.class); ~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: platform type 'kotlin.io.FileTreeWalk' should not be mocked [DoNotMockPlatformTypes] mock(FileTreeWalk.class); ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:17: Error: platform type 'kotlin.Lazy' should not be mocked [DoNotMockPlatformTypes] mock(Lazy.class); ~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:19: Error: platform type 'android.graphics.Typeface' should not be mocked [DoNotMockPlatformTypes] mock(Typeface.class); ~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:21: Error: platform type 'androidx.collection.ArrayMap' should not be mocked [DoNotMockPlatformTypes] mock(ArrayMap.class); ~~~~~~~~~~~~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/RecordClassMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Test import slack.lint.BaseSlackLintTest class RecordClassMockDetectorTest : BaseSlackLintTest() { private val testClass = kotlin( """ package slack.test import kotlin.jvm.JvmRecord @JvmRecord data class TestClass(val foo: String) """ ) .indented() private val testJavaClass = java( "test/slack/test/TestJavaClass.java", """ package slack.test; record TestJavaClass(String foo) { } """, ) .indented() private val testClasses = arrayOf( testClass // TODO test once lint supports java record // https://issuetracker.google.com/issues/283693337 // testJavaClass ) override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val assigned: TestClass = mock() val fake = TestClass("this is fine") } // TODO uncomment once above Java record support is fixed // @Mock lateinit var fieldMock2: TestJavaClass // @Spy lateinit var fieldSpy2: TestJavaClass // // fun example2() { // val localMock1 = org.mockito.Mockito.mock(TestJavaClass::class.java) // val localSpy1 = org.mockito.Mockito.spy(localMock1) // val localMock2 = mock() // val classRef = TestJavaClass::class.java // val localMock3 = org.mockito.Mockito.mock(classRef) // // val dynamicMock = mock { // // } // val assigned: TestJavaClass = mock() // val fake = TestJavaClass("this is fine") // } } """, ) .indented() lint() .files(*mockFileStubs(), *testClasses, source) .run() .expect( """ test/test/slack/test/TestClass.kt:8: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] @Mock lateinit var fieldMock: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] @Spy lateinit var fieldSpy: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:12: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:13: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:14: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] val localMock2 = mock() ~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] val dynamicMock = mock { ^ test/test/slack/test/TestClass.kt:21: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] val assigned: TestClass = mock() ~~~~~~ 8 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass fake = new TestClass("this is fine"); } // TODO uncomment once above Java record support is fixed // @Mock TestJavaClass fieldMock2; // @Spy TestJavaClass fieldSpy2; // // public void example2() { // TestJavaClass localMock = mock(TestJavaClass.class); // TestJavaClass localSpy = spy(localMock); // Class classRef = TestJavaClass.class; // TestJavaClass localMock2 = mock(classRef); // TestJavaClass fake = new TestJavaClass("this is fine"); // } } """, ) .indented() lint() .files(*mockFileStubs(), *testClasses, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] @Mock TestClass fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] @Spy TestClass fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:13: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] TestClass localMock = mock(TestClass.class); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:14: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] TestClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: 'slack.test.TestClass' is a record class, so mocking it should not be necessary [DoNotMockRecordClass] TestClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/SealedClassMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import com.android.tools.lint.checks.infrastructure.TestMode import com.intellij.pom.java.LanguageLevel import org.junit.Test import slack.lint.BaseSlackLintTest class SealedClassMockDetectorTest : BaseSlackLintTest() { private val testClass = kotlin( """ package slack.test sealed class TestClass """ ) .indented() private val javaTestClass = java( """ package slack.test; sealed interface TestJavaClass permits TestJavaClass.Subtype1 { interface Subtype1 { } } """ ) .indented() private val testClasses = arrayOf(testClass, javaTestClass) override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() override val skipTestModes: Array // I don't understand the point of this mode get() = arrayOf(TestMode.SUPPRESSIBLE) @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass fun example() { val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val assigned: TestClass = mock() val fake = TestClass("this is fine") } @Mock lateinit var fieldMock2: TestJavaClass @Spy lateinit var fieldSpy2: TestJavaClass fun example2() { val localMock1 = org.mockito.Mockito.mock(TestJavaClass::class.java) val localSpy1 = org.mockito.Mockito.spy(localMock1) val localMock2 = mock() val classRef = TestJavaClass::class.java val localMock3 = org.mockito.Mockito.mock(classRef) val dynamicMock = mock { } val assigned: TestJavaClass = mock() val fake = TestJavaClass("this is fine") } } """, ) .indented() lint() .javaLanguageLevel(LanguageLevel.JDK_17) .files(*mockFileStubs(), *testClasses, source) .run() .expect( """ test/test/slack/test/TestClass.kt:8: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Mock lateinit var fieldMock: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Spy lateinit var fieldSpy: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:12: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:13: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:14: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock2 = mock() ~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val dynamicMock = mock { ^ test/test/slack/test/TestClass.kt:21: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val assigned: TestClass = mock() ~~~~~~ test/test/slack/test/TestClass.kt:25: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Mock lateinit var fieldMock2: TestJavaClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:26: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Spy lateinit var fieldSpy2: TestJavaClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:28: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock1 = org.mockito.Mockito.mock(TestJavaClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:29: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:30: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock2 = mock() ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:32: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:34: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val dynamicMock = mock { ^ test/test/slack/test/TestClass.kt:37: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] val assigned: TestJavaClass = mock() ~~~~~~ 16 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass fake = new TestClass("this is fine"); } @Mock TestJavaClass fieldMock2; @Spy TestJavaClass fieldSpy2; public void example2() { TestClass localMock = mock(TestJavaClass.class); TestJavaClass localSpy = spy(localMock); Class classRef = TestJavaClass.class; TestJavaClass localMock2 = mock(classRef); TestJavaClass fake = new TestJavaClass("this is fine"); } } """, ) .indented() lint() .javaLanguageLevel(LanguageLevel.JDK_17) .files(*mockFileStubs(), *testClasses, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Mock TestClass fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Spy TestClass fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:13: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] TestClass localMock = mock(TestClass.class); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:14: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] TestClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] TestClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:20: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Mock TestJavaClass fieldMock2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:21: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] @Spy TestJavaClass fieldSpy2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:23: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] TestClass localMock = mock(TestJavaClass.class); ~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:24: Error: 'slack.test.TestClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] TestJavaClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:26: Error: 'slack.test.TestJavaClass' is a sealed type and has a restricted type hierarchy, use a subtype instead. [DoNotMockSealedClass] TestJavaClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ 10 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/mocking/ValueClassMockDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.mocking import org.junit.Ignore import org.junit.Test import slack.lint.BaseSlackLintTest @Ignore("https://issuetracker.google.com/issues/283715187") class ValueClassMockDetectorTest : BaseSlackLintTest() { private val testClass = kotlin( """ package slack.test import kotlin.jvm.JvmInline @JvmInline value class TestClass(val foo: String) """ ) .indented() override fun getDetector() = MockDetector() override fun getIssues() = MockDetector.ISSUES.toList() @Test fun kotlinTests() { val source = kotlin( "test/test/slack/test/TestClass.kt", """ package slack.test import org.mockito.Mock import org.mockito.Spy import slack.test.mockito.mock class MyTests { @Mock lateinit var fieldMock: TestClass @Spy lateinit var fieldSpy: TestClass fun example() { // val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) val localSpy1 = org.mockito.Mockito.spy(1u) // val localMock2 = mock() // val classRef = TestClass::class.java // val localMock3 = org.mockito.Mockito.mock(classRef) // val dynamicMock = mock { // // } // val assigned: TestClass = mock() // val fake = TestClass("this is fine") } } """, ) .indented() lint() .files(*mockFileStubs(), testClass, source) .run() .expect( """ test/test/slack/test/TestClass.kt:8: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] @Mock lateinit var fieldMock: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:9: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] @Spy lateinit var fieldSpy: TestClass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:12: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] val localMock1 = org.mockito.Mockito.mock(TestClass::class.java) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:13: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] val localSpy1 = org.mockito.Mockito.spy(localMock1) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:14: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] val localMock2 = mock() ~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:16: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] val localMock3 = org.mockito.Mockito.mock(classRef) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.kt:18: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] val dynamicMock = mock { ^ test/test/slack/test/TestClass.kt:21: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] val assigned: TestClass = mock() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 errors, 0 warnings """ .trimIndent() ) } @Test fun javaTests() { val source = java( "test/test/slack/test/TestClass.java", """ package slack.test; import org.mockito.Mock; import org.mockito.Spy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; class MyTests { @Mock TestClass fieldMock; @Spy TestClass fieldSpy; public void example() { TestClass localMock = mock(TestClass.class); TestClass localSpy = spy(localMock); Class classRef = TestClass.class; TestClass localMock2 = mock(classRef); TestClass fake = new TestClass("this is fine"); } } """, ) .indented() lint() .files(*mockFileStubs(), testClass, source) .run() .expect( """ test/test/slack/test/TestClass.java:9: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] @Mock TestClass fieldMock; ~~~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:10: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] @Spy TestClass fieldSpy; ~~~~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:13: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] TestClass localMock = mock(TestClass.class); ~~~~~~~~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:14: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] TestClass localSpy = spy(localMock); ~~~~~~~~~~~~~~ test/test/slack/test/TestClass.java:16: Error: value classes represent inlined types, so mocking them should not be necessary [DoNotMockValueClass] TestClass localMock2 = mock(classRef); ~~~~~~~~~~~~~~ 5 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/parcel/ParcelizeFunctionPropertyDetectorTest.kt ================================================ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.parcel import org.junit.Test import slack.lint.BaseSlackLintTest class ParcelizeFunctionPropertyDetectorTest : BaseSlackLintTest() { companion object { private val PARCELIZE_STUBS = kotlin( "test/kotlinx/parcelize/Parcelize.kt", """ package kotlinx.parcelize annotation class Parcelize annotation class IgnoredOnParcel """, ) .indented() } override fun getDetector() = ParcelizeFunctionPropertyDetector() override fun getIssues() = listOf(ParcelizeFunctionPropertyDetector.ISSUE) @Test fun simple() { lint() .files( PARCELIZE_STUBS, kotlin( """ package test.pkg import android.os.Parcelable import kotlinx.parcelize.Parcelize import kotlinx.parcelize.IgnoredOnParcel typealias FunctionType = () -> String @Parcelize class Example1( val foo: String, val functionType1: () -> String, val functionType2: (String) -> String, val functionType3: String.() -> String, val functionType4: () -> Unit, val functionType5: suspend () -> Unit, val aliasedFunction: FunctionType, val functionClass: FunctionClass, @IgnoredOnParcel val ignoredFunction: () -> Unit = {}, // This is allowed ) : Parcelable """ ) .indented(), ) .allowCompilationErrors(false) .run() .expect( """ src/test/pkg/Example1.kt:12: Error: While technically (and surprisingly) supported by Parcelize, function types should not be used in Parcelize classes. There are only limited conditions where it will work and it's usually a sign that you're modeling your data wrong. [ParcelizeFunctionProperty] val functionType1: () -> String, ~~~~~~~~~~~~ src/test/pkg/Example1.kt:13: Error: While technically (and surprisingly) supported by Parcelize, function types should not be used in Parcelize classes. There are only limited conditions where it will work and it's usually a sign that you're modeling your data wrong. [ParcelizeFunctionProperty] val functionType2: (String) -> String, ~~~~~~~~~~~~~~~~~~ src/test/pkg/Example1.kt:14: Error: While technically (and surprisingly) supported by Parcelize, function types should not be used in Parcelize classes. There are only limited conditions where it will work and it's usually a sign that you're modeling your data wrong. [ParcelizeFunctionProperty] val functionType3: String.() -> String, ~~~~~~~~~~~~~~~~~~~ src/test/pkg/Example1.kt:15: Error: While technically (and surprisingly) supported by Parcelize, function types should not be used in Parcelize classes. There are only limited conditions where it will work and it's usually a sign that you're modeling your data wrong. [ParcelizeFunctionProperty] val functionType4: () -> Unit, ~~~~~~~~~~ src/test/pkg/Example1.kt:16: Error: While technically (and surprisingly) supported by Parcelize, function types should not be used in Parcelize classes. There are only limited conditions where it will work and it's usually a sign that you're modeling your data wrong. [ParcelizeFunctionProperty] val functionType5: suspend () -> Unit, ~~~~~~~~~~~~~~~~~~ src/test/pkg/Example1.kt:17: Error: While technically (and surprisingly) supported by Parcelize, function types should not be used in Parcelize classes. There are only limited conditions where it will work and it's usually a sign that you're modeling your data wrong. [ParcelizeFunctionProperty] val aliasedFunction: FunctionType, ~~~~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/resources/FullyQualifiedResourceDetectorTest.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import org.junit.Test import slack.lint.BaseSlackLintTest import slack.lint.resources.ImportAliasesLoader.IMPORT_ALIASES class FullyQualifiedResourceDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = FullyQualifiedResourceDetector() override fun getIssues() = listOf(FullyQualifiedResourceDetector.ISSUE) override fun lint(): TestLintTask { return super.lint() .configureOption( IMPORT_ALIASES, "slack.l10n.R as L10nR, slack.uikit.resources.R as SlackKitR, slack.uikit.R as UiKitR", ) } @Test fun `test success`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R as L10nR class MyClass { init { val appName = getString(L10R.string.app_name) } } """ ) .indented() ) .run() .expectClean() } @Test fun `test failure no imports`() { lint() .files( kotlin( """ package slack.pkg.subpackage class MyClass { init { val appName = getString(slack.l10n.R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:6: Error: Use L10nR as an import alias instead [FullyQualifiedResource] val appName = getString(slack.l10n.R.string.app_name) ~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 6: Replace with import alias: @@ -3 +3 + import slack.l10n.R as L10nR + @@ -6 +8 - val appName = getString(slack.l10n.R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure no import`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.pkg.R class MyClass { init { val appName = getString(slack.l10n.R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:8: Error: Use L10nR as an import alias instead [FullyQualifiedResource] val appName = getString(slack.l10n.R.string.app_name) ~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 8: Replace with import alias: @@ -4 +4 + import slack.l10n.R as L10nR @@ -8 +9 - val appName = getString(slack.l10n.R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure import`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.pkg.R import slack.l10n.R as L10nR class MyClass { init { val appName = getString(slack.l10n.R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:9: Error: Use L10nR as an import alias instead [FullyQualifiedResource] val appName = getString(slack.l10n.R.string.app_name) ~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 9: Replace with import alias: @@ -9 +9 - val appName = getString(slack.l10n.R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure import without alias`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R class MyClass { init { val appName = getString(slack.l10n.R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:8: Error: Use L10nR as an import alias instead [FullyQualifiedResource] val appName = getString(slack.l10n.R.string.app_name) ~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 8: Replace with import alias: @@ -4 +4 + import slack.l10n.R as L10nR @@ -8 +9 - val appName = getString(slack.l10n.R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure import wrong alias`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R as L10R class MyClass { init { val appName = getString(slack.l10n.R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:8: Error: Use L10nR as an import alias instead [FullyQualifiedResource] val appName = getString(slack.l10n.R.string.app_name) ~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 8: Replace with import alias: @@ -4 +4 + import slack.l10n.R as L10nR @@ -8 +9 - val appName = getString(slack.l10n.R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure no fix`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R as L10R class MyClass { init { val appName = getString(slack.pkg.R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:8: Error: Use an import alias instead [FullyQualifiedResource] val appName = getString(slack.pkg.R.string.app_name) ~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs("") } @Test fun `test java no-op`() { lint() .files( java( """ package slack.pkg.subpackage; class MyClass { MyClass(){ String appName = getString(slack.l10n.R.string.app_name); } } """ ) .indented() ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/resources/MissingResourceImportAliasDetectorTest.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import org.junit.Test import slack.lint.BaseSlackLintTest class MissingResourceImportAliasDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = MissingResourceImportAliasDetector() override fun getIssues() = listOf(MissingResourceImportAliasDetector.ISSUE) override fun lint(): TestLintTask { return super.lint() .configureOption( ImportAliasesLoader.IMPORT_ALIASES, "slack.l10n.R as L10nR, slack.uikit.resources.R as SlackKitR, slack.uikit.R as UiKitR", ) } @Test fun `test success`() { lint() .files( kotlin( """ package lint.test.pkg.subpackage import slack.l10n.R as L10nR import lint.test.pkg.R class MyClass { init { val appName = getString(L10nR.string.app_name) } } """ ) .indented() ) .run() .expectClean() } @Test fun `test failure no references`() { lint() .files( kotlin( """ package lint.test.pkg import slack.l10n.R class MyClass """ ) .indented() ) .run() .expect( """ src/lint/test/pkg/MyClass.kt:3: Error: Use an import alias for R classes from other modules [MissingResourceImportAlias] import slack.l10n.R ~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/lint/test/pkg/MyClass.kt line 3: Add import alias: @@ -3 +3 - import slack.l10n.R + import slack.l10n.R as L10nR """ .trimIndent() ) } @Test fun `test failure one reference`() { lint() .files( kotlin( """ package lint.test.pkg import slack.l10n.R class MyClass { init { val appName = getString(R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/lint/test/pkg/MyClass.kt:3: Error: Use an import alias for R classes from other modules [MissingResourceImportAlias] import slack.l10n.R ~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/lint/test/pkg/MyClass.kt line 3: Add import alias: @@ -3 +3 - import slack.l10n.R + import slack.l10n.R as L10nR @@ -8 +8 - val appName = getString(R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure multiple references`() { lint() .files( kotlin( """ package lint.test.pkg import slack.l10n.R class MyClass { init { val appName = getString(R.string.app_name) + "-" + getString(R.string.suffix) getColor(R.color.transparent).let { println(it) } } } """ ) .indented() ) .run() .expect( """ src/lint/test/pkg/MyClass.kt:3: Error: Use an import alias for R classes from other modules [MissingResourceImportAlias] import slack.l10n.R ~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/lint/test/pkg/MyClass.kt line 3: Add import alias: @@ -3 +3 - import slack.l10n.R + import slack.l10n.R as L10nR @@ -8 +8 - val appName = getString(R.string.app_name) + "-" + getString(R.string.suffix) - getColor(R.color.transparent).let { println(it) } + val appName = getString(L10nR.string.app_name) + "-" + getString(L10nR.string.suffix) + getColor(L10nR.color.transparent).let { println(it) } """ .trimIndent() ) } @Test fun `test failure VERSION_CODES R reference`() { lint() .files( kotlin( """ package lint.test.pkg import slack.l10n.R class MyClass { init { val appName = getString(R.string.app_name) + "-" + getString(R.string.suffix) if (isAtLeastApi(Build.VERSION_CODES.R)) { getColor(R.color.transparent).let { println(it) } } } } """ ) .indented() ) .run() .expect( """ src/lint/test/pkg/MyClass.kt:3: Error: Use an import alias for R classes from other modules [MissingResourceImportAlias] import slack.l10n.R ~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/lint/test/pkg/MyClass.kt line 3: Add import alias: @@ -3 +3 - import slack.l10n.R + import slack.l10n.R as L10nR @@ -8 +8 - val appName = getString(R.string.app_name) + "-" + getString(R.string.suffix) + val appName = getString(L10nR.string.app_name) + "-" + getString(L10nR.string.suffix) @@ -10 +10 - getColor(R.color.transparent).let { println(it) } + getColor(L10nR.color.transparent).let { println(it) } """ .trimIndent() ) } @Test fun `test failure no fix`() { lint() .files( kotlin( """ package lint.test.pkg import lint.test.subpkg.R class MyClass { init { val appName = getString(R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/lint/test/pkg/MyClass.kt:3: Error: Use an import alias for R classes from other modules [MissingResourceImportAlias] import lint.test.subpkg.R ~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs("") } @Test fun `test java no-op`() { lint() .files( java( """ package lint.test.pkg; import lint.test.subpkg.R; class MyClass { MyClass() { String appName = getString(R.string.app_name); } } """ ) .indented() ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/resources/WrongResourceImportAliasDetectorTest.kt ================================================ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.resources import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import org.junit.Test import slack.lint.BaseSlackLintTest class WrongResourceImportAliasDetectorTest : BaseSlackLintTest() { override fun getDetector(): Detector = WrongResourceImportAliasDetector() override fun getIssues() = listOf(WrongResourceImportAliasDetector.ISSUE) override fun lint(): TestLintTask { return super.lint() .configureOption( ImportAliasesLoader.IMPORT_ALIASES, "slack.l10n.R as L10nR, slack.uikit.resources.R as SlackKitR, slack.uikit.R as UiKitR", ) } @Test fun `test success`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R as L10nR import slack.pkg.R class MyClass { init { val appName = getString(L10R.string.app_name) } } """ ) .indented() ) .run() .expectClean() } @Test fun `test failure no references`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R as L10R class MyClass """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:3: Error: Use L10nR as an import alias here [WrongResourceImportAlias] import slack.l10n.R as L10R ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 3: Replace import alias: @@ -3 +3 - import slack.l10n.R as L10R + import slack.l10n.R as L10nR """ .trimIndent() ) } @Test fun `test failure one reference`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.uikit.resources.R as SlackKitR import slack.l10n.R as L10R class MyClass { init { val appName = getString(L10R.string.app_name) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:4: Error: Use L10nR as an import alias here [WrongResourceImportAlias] import slack.l10n.R as L10R ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 4: Replace import alias: @@ -4 +4 - import slack.l10n.R as L10R + import slack.l10n.R as L10nR @@ -9 +9 - val appName = getString(L10R.string.app_name) + val appName = getString(L10nR.string.app_name) """ .trimIndent() ) } @Test fun `test failure multiple references`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.l10n.R as L10R class MyClass { init { val appName = getString(L10R.string.app_name) + "-" + getString(L10R.string.suffix) getColor(R.color.transparent).let { println(it) } } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:3: Error: Use L10nR as an import alias here [WrongResourceImportAlias] import slack.l10n.R as L10R ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 3: Replace import alias: @@ -3 +3 - import slack.l10n.R as L10R + import slack.l10n.R as L10nR @@ -8 +8 - val appName = getString(L10R.string.app_name) + "-" + getString(L10R.string.suffix) + val appName = getString(L10nR.string.app_name) + "-" + getString(L10nR.string.suffix) """ .trimIndent() ) } @Test fun `test failure multiple wrong imports`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.uikit.resources.R as SKR import slack.l10n.R as L10R class MyClass { init { getColor(SKR.color.transparent).let { println(it) } val appName = getString(L10R.string.app_name) + "-" + getString(L10R.string.suffix) } } """ ) .indented() ) .run() .expect( """ src/slack/pkg/subpackage/MyClass.kt:3: Error: Use SlackKitR as an import alias here [WrongResourceImportAlias] import slack.uikit.resources.R as SKR ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/slack/pkg/subpackage/MyClass.kt line 3: Replace import alias: @@ -3 +3 - import slack.uikit.resources.R as SKR + import slack.uikit.resources.R as SlackKitR @@ -9 +9 - getColor(SKR.color.transparent).let { println(it) } + getColor(SlackKitR.color.transparent).let { println(it) } """ .trimIndent() ) } @Test fun `test no fix`() { lint() .files( kotlin( """ package slack.pkg.subpackage import slack.pkg.subpkg.R as SubPkgR class MyClass { init { val appName = getString(SubPkgR.string.app_name) } } """ ) .indented() ) .run() .expectClean() } @Test fun `test java no-op`() { lint() .files( java( """ package slack.pkg.subpackage; import slack.l10n.R; class MyClass { MyClass() { String appName = getString(R.string.app_name); } } """ ) .indented() ) .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/retrofit/RetrofitJarLoader.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.retrofit import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile import java.io.File import slack.lint.BaseSlackLintTest /** Loads the test Retrofit 3 jar from resources. */ fun BaseSlackLintTest.retrofit3Jar() = LibraryReferenceTestFile(File(javaClass.classLoader.getResource("retrofit-3.0.0.jar")!!.toURI())) ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/retrofit/RetrofitUsageDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.retrofit import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.detector.api.Detector import org.junit.Test import slack.lint.BaseSlackLintTest class RetrofitUsageDetectorTest : BaseSlackLintTest() { private companion object { val allowUnitResult: TestFile = kotlin( """ package slack.lint.annotations @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.SOURCE) annotation class AllowUnitResult """ ) .indented() } private val retrofit3Jar = retrofit3Jar() override fun getDetector(): Detector = RetrofitUsageDetector() override fun getIssues() = listOf(RetrofitUsageDetector.ISSUE) @Test fun formEncoding() { lint() .files( retrofit3Jar, kotlin( """ package test import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.POST interface Example { @GET("/") @FormUrlEncoded fun wrongMethod(): String @POST("/") @FormUrlEncoded fun missingFieldParams(): String @POST("/") fun missingAnnotation(@Field("hi") input: String): String @FormUrlEncoded @POST("/") fun correct(@Field("hi") input: String): String } """ ) .indented(), ) .run() .expect( """ src/test/Example.kt:11: Error: @FormUrlEncoded requires @PUT, @POST, or @PATCH. [RetrofitUsage] fun wrongMethod(): String ~~~~~~~~~~~ src/test/Example.kt:14: Error: @FormUrlEncoded but has no @Field(Map) parameters. [RetrofitUsage] @FormUrlEncoded ~~~~~~~~~~~~~~~ src/test/Example.kt:18: Error: @Field(Map) param requires @FormUrlEncoded. [RetrofitUsage] fun missingAnnotation(@Field("hi") input: String): String ~~~~~~~~~~~~~~~~~ 3 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Autofix for src/test/Example.kt line 14: Remove '@FormUrlEncoded': @@ -14 +14 - @FormUrlEncoded Autofix for src/test/Example.kt line 18: Replace with @retrofit2.http.FormUrlEncoded...: @@ -17 +17 - @POST("/") + @retrofit2.http.FormUrlEncoded + @POST("/") """ .trimIndent() ) } @Test fun bodies() { lint() .files( retrofit3Jar, kotlin( """ package test import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.Part import retrofit2.http.POST interface Example { @GET("/") fun wrongMethod(@Body body : String): String @POST("/") fun missingBody(): String @POST("/") fun doubleBody(@Body input: String, @Body input2: String): String @POST("/") fun correct(@Body input: String): String @Multipart @POST("/") fun multipartCorrect(@Part input: String): String @Multipart @GET("/") fun multipartBadMethod(@Part input: String): String @Multipart @POST("/") fun multipartBadParameterType(@Body input: String): String @Multipart @POST("/") fun multipartMissingPartParameter(): String } """ ) .indented(), ) .run() .expect( """ src/test/Example.kt:10: Error: @Body param requires @PUT, @POST, or @PATCH. [RetrofitUsage] @GET("/") ~~~~~~~~~ src/test/Example.kt:13: Error: This annotation requires an @Body parameter. [RetrofitUsage] @POST("/") ~~~~~~~~~~ src/test/Example.kt:17: Error: Duplicate @Body param!. [RetrofitUsage] fun doubleBody(@Body input: String, @Body input2: String): String ~~~~~~~~~~~~~~~~~~~~ src/test/Example.kt:28: Error: @Multipart requires @PUT, @POST, or @PATCH. [RetrofitUsage] fun multipartBadMethod(@Part input: String): String ~~~~~~~~~~~~~~~~~~ src/test/Example.kt:31: Error: @Multipart methods should only contain @Part parameters. [RetrofitUsage] @POST("/") ~~~~~~~~~~ src/test/Example.kt:35: Error: @Multipart methods should contain at least one @Part parameter. [RetrofitUsage] @POST("/") ~~~~~~~~~~ 6 errors, 0 warnings """ .trimIndent() ) } @Test fun unitReturn() { lint() .files( retrofit3Jar, allowUnitResult, kotlin( """ package test import retrofit2.http.GET import slack.lint.annotations.AllowUnitResult interface Example { @GET("/") fun unitMethod() @GET("/") suspend fun suspendUnitMethod() @GET("/") fun unitMethodExplicit(): Unit suspend fun suspendUnitMethodExplicit(): Unit @AllowUnitResult @PUT("/") suspend fun suspendUnitMethodAllowUnitResult() @AllowUnitResult @DELETE("/") suspend fun suspendUnitMethodExplicitAllowUnitResult(): Unit } """ ) .indented(), ) .run() .expect( """ src/test/Example.kt:8: Error: Retrofit endpoints should return something other than Unit/void. [RetrofitUsage] fun unitMethod() ~~~~~~~~~~ src/test/Example.kt:11: Error: Retrofit endpoints should return something other than Unit/void. [RetrofitUsage] suspend fun suspendUnitMethod() ~~~~~~~~~~~~~~~~~ src/test/Example.kt:14: Error: Retrofit endpoints should return something other than Unit/void. [RetrofitUsage] fun unitMethodExplicit(): Unit ~~~~~~~~~~~~~~~~~~ 3 errors """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/rx/RxJavaJarLoader.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.rx import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile import java.io.File import slack.lint.BaseSlackLintTest /** Loads the test RxJava 3 jar from resources. */ fun BaseSlackLintTest.rxJavaJar3() = LibraryReferenceTestFile(File(javaClass.classLoader.getResource("rxjava-3.1.0.jar")!!.toURI())) ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/rx/RxObservableEmitDetectorTest.kt ================================================ // Copyright (C) 2025 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.rx import com.android.tools.lint.checks.infrastructure.TestFile import java.util.Locale.getDefault import org.junit.Test import slack.lint.BaseSlackLintTest class RxObservableEmitDetectorTest : BaseSlackLintTest() { override fun getDetector() = RxObservableEmitDetector() override fun getIssues() = RxObservableEmitDetector.issues @Test fun testDocumentationExample() { testWhenResultsOfSendAreReturned(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - does not call send or trySend`() { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.rxObservable class Foo { fun foo() { rxObservable { println("foo") } } } """ ) .indented(), ) .run() .expect( """ src/test/Foo.kt:7: Hint: rxObservable does not call send() or trySend() [RxObservableDoesNotEmit] rxObservable { println("foo") } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 0 warnings, 1 hint """ .trimIndent() ) } @Test fun `rxFlowable - does not call send or trySend`() { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.rxFlowable class Foo { fun foo() { rxFlowable { println("foo") } } } """ ) .indented(), ) .run() .expect( """ src/test/Foo.kt:7: Hint: rxFlowable does not call send() or trySend() [RxFlowableDoesNotEmit] rxFlowable { println("foo") } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 0 warnings, 1 hint """ .trimIndent() ) } @Test fun `rxObservable - calls send`() { testWhenResultsOfSendAreReturned(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - calls trySend`() { testWhenResultsOfSendAreReturned(RX_OBSERVABLE, TRY_SEND) } @Test fun `rxFlowable - calls send`() { testWhenResultsOfSendAreReturned("rxFlowable", SEND) } @Test fun `rxFlowable - calls trySend`() { testWhenResultsOfSendAreReturned("rxFlowable", TRY_SEND) } @Test fun `rxObservable - results of send are not the lambda return value`() { testWhenResultsOfSendAreNotReturned(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - results of trySend are not the lambda return value`() { testWhenResultsOfSendAreNotReturned(RX_OBSERVABLE, TRY_SEND) } @Test fun `rxFlowable - results of send are not the lambda return value`() { testWhenResultsOfSendAreNotReturned("rxFlowable", SEND) } @Test fun `rxFlowable - results of trySend are not the lambda return value`() { testWhenResultsOfSendAreNotReturned("rxFlowable", TRY_SEND) } private fun testWhenResultsOfSendAreReturned(method: String, emitter: String) { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.$method class Foo { fun foo() { $method { $emitter("foo") } } } """ ) .indented(), ) .run() .expectClean() } private fun testWhenResultsOfSendAreNotReturned(method: String, emitter: String) { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.$method class Foo { fun foo() { $method { $emitter("foo") println("bar") } } } """ ) .indented(), ) .run() .expectClean() } @Test fun `rxObservable - should fail when outer factor does not call send`() { testWhenOuterFactoryDoesNotCallSend(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - should fail when outer factory does not call trySend`() { testWhenOuterFactoryDoesNotCallSend(RX_OBSERVABLE, TRY_SEND) } @Test fun `rxFlowable - should fail when outer factor does not call send`() { testWhenOuterFactoryDoesNotCallSend(RX_FLOWABLE, SEND) } @Test fun `rxFlowable - should fail when outer factory does not call trySend`() { testWhenOuterFactoryDoesNotCallSend(RX_FLOWABLE, TRY_SEND) } private fun testWhenOuterFactoryDoesNotCallSend(method: String, emitter: String) { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.$method class Foo { fun foo() { $method { $method { $emitter("foo") } } } } """ ) .indented(), ) .run() .expect( """ src/test/Foo.kt:7: Hint: $method does not call send() or trySend() [${getError(method)}] $method { ^ 0 errors, 0 warnings, 1 hint """ .trimIndent() ) } @Test fun `rxObservable - should fail when inner factor does not call send`() { testWhenInnerFactoryDoesNotCallSend(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - should fail when inner factory does not call trySend`() { testWhenInnerFactoryDoesNotCallSend(RX_OBSERVABLE, TRY_SEND) } @Test fun `rxFlowable - should fail when inner factor does not call send`() { testWhenInnerFactoryDoesNotCallSend(RX_FLOWABLE, SEND) } @Test fun `rxFlowable - should fail when inner factory does not call trySend`() { testWhenInnerFactoryDoesNotCallSend(RX_FLOWABLE, TRY_SEND) } private fun testWhenInnerFactoryDoesNotCallSend(method: String, emitter: String) { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.$method class Foo { fun foo() { $method { $emitter("foo") $method { println("bar!") } } } } """ ) .indented(), ) .run() .expect( """ src/test/Foo.kt:9: Hint: $method does not call send() or trySend() [${getError(method)}] $method { ^ 0 errors, 0 warnings, 1 hint """ .trimIndent() ) } @Test fun `rxObservable - should succeed when inner and outer call send`() { testWhenInnerAndOuterFactoriesCallSend(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - should succeed when inner and outer call trySend`() { testWhenInnerAndOuterFactoriesCallSend(RX_OBSERVABLE, TRY_SEND) } @Test fun `rxFlowable - should succeed when inner and outer call send`() { testWhenInnerAndOuterFactoriesCallSend(RX_FLOWABLE, SEND) } @Test fun `rxFlowable - should succeed when inner and outer call trySend`() { testWhenInnerAndOuterFactoriesCallSend(RX_FLOWABLE, TRY_SEND) } /** * @param method a function like "rxObservable" * @param emitter an emitter function like "send" */ private fun testWhenInnerAndOuterFactoriesCallSend(method: String, emitter: String) { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.$method class Foo { fun foo() { $method { $emitter("foo") $method { $emitter("foo") } } } } """ ) .indented(), ) .run() .expectClean() } @Test fun `rxObservable - should succeed when aliased factory calls send`() { testWhenAliasedFactoryCallsSend(RX_OBSERVABLE, SEND) } @Test fun `rxObservable - should succeed when aliased factory calls trySend`() { testWhenAliasedFactoryCallsSend(RX_OBSERVABLE, TRY_SEND) } @Test fun `rxFlowable - should succeed when aliased factory calls send`() { testWhenAliasedFactoryCallsSend(RX_FLOWABLE, SEND) } @Test fun `rxFlowable - should succeed when aliased factory calls trySend`() { testWhenAliasedFactoryCallsSend(RX_FLOWABLE, TRY_SEND) } private fun testWhenAliasedFactoryCallsSend(method: String, emitter: String) { lint() .files( *files, kotlin( """ package test import kotlinx.coroutines.rx3.$method as factory class Foo { fun foo() { factory { $emitter("foo") } } } """ ) .indented(), ) .run() .expectClean() } private fun getError(method: String) = method.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() } + "DoesNotEmit" private companion object { const val RX_OBSERVABLE = "rxObservable" const val RX_FLOWABLE = "rxFlowable" const val SEND = "send" const val TRY_SEND = "trySend" val COROUTINE_CONTEXT_API: TestFile = kotlin( """ package kotlin.coroutines interface CoroutineContext """ ) .indented() val PRODUCER_SCOPE_API: TestFile = kotlin( """ package kotlin.coroutines class ChannelResult interface ProducerScope { suspend fun send(element: E) fun trySend(element: E): ChannelResult } """ ) .indented() val RX_OBSERVABLE_API: TestFile = kotlin( """ package kotlinx.coroutines.rx3 import io.reactivex.rxjava3.core.Observable import kotlin.coroutines.CoroutineContext import kotlin.coroutines.ProducerScope fun rxObservable( context: CoroutineContext, block: suspend ProducerScope.() -> Unit ): Observable """ ) .indented() val RX_FLOWABLE_API: TestFile = kotlin( """ package kotlinx.coroutines.rx3 import io.reactivex.rxjava3.core.Flowable import kotlin.coroutines.CoroutineContext import kotlin.coroutines.ProducerScope fun rxFlowable( context: CoroutineContext, block: suspend ProducerScope.() -> Unit ): Flowable """ ) .indented() val OBSERVABLE_API: TestFile = kotlin( """ package io.reactivex.rxjava3.core import io.reactivex.rxjava4.annotations.NonNull interface Observable<@NonNull T> """ ) .indented() val FLOWABLE_API: TestFile = kotlin( """ package io.reactivex.rxjava3.core import io.reactivex.rxjava4.annotations.NonNull interface Flowable<@NonNull T> """ ) .indented() val NON_NULL_API: TestFile = kotlin( """ package io.reactivex.rxjava4.annotations annotation class NonNull """ ) .indented() val files = arrayOf( COROUTINE_CONTEXT_API, PRODUCER_SCOPE_API, RX_OBSERVABLE_API, RX_FLOWABLE_API, OBSERVABLE_API, FLOWABLE_API, NON_NULL_API, ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/rx/RxSubscribeOnMainDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.rx import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test import slack.lint.BaseSlackLintTest class RxSubscribeOnMainDetectorTest : BaseSlackLintTest() { private val rxJavaJar3 = rxJavaJar3() private val androidSchedulers = java( """ package io.reactivex.rxjava3.android.schedulers; import io.reactivex.rxjava3.core.Scheduler; public final class AndroidSchedulers { public static Scheduler mainThread() { return null; } } """ ) .indented() override val skipTestModes: Array = arrayOf(TestMode.WHITESPACE, TestMode.PARENTHESIZED) override fun getDetector() = RxSubscribeOnMainDetector() override fun getIssues() = listOf(RxSubscribeOnMainDetector.ISSUE) @Test fun subscribeOnMain_fullyQualified_fails_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; public class Foo { public void bar(Observable obs) { obs.subscribeOn(AndroidSchedulers.mainThread()); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.java:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(AndroidSchedulers.mainThread()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.java line 8: Replace with observeOn(): @@ -8 +8 - obs.subscribeOn(AndroidSchedulers.mainThread()); + obs.observeOn(AndroidSchedulers.mainThread()); """ .trimIndent() ) } @Test fun subscribeOnMain_fullyQualified_fails_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { fun bar(obs: Observable) { obs.subscribeOn(AndroidSchedulers.mainThread()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(AndroidSchedulers.mainThread()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 8: Replace with observeOn(): @@ -8 +8 - obs.subscribeOn(AndroidSchedulers.mainThread()) + obs.observeOn(AndroidSchedulers.mainThread()) """ .trimIndent() ) } @Test fun subscribeOnMain_staticImport_fails_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import static io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread; public class Foo { public void bar(Observable obs) { obs.subscribeOn(mainThread()); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.java:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(mainThread()); ~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.java line 8: Replace with observeOn(): @@ -8 +8 - obs.subscribeOn(mainThread()); + obs.observeOn(mainThread()); """ .trimIndent() ) } @Test fun subscribeOnMain_staticImport_fails_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread class Foo { fun bar(obs: Observable) { obs.subscribeOn(mainThread()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(mainThread()) ~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 8: Replace with observeOn(): @@ -8 +8 - obs.subscribeOn(mainThread()) + obs.observeOn(mainThread()) """ .trimIndent() ) } @Test fun subscribeOnMain_outsideAssignment_field_fails_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; public class Foo { private final Scheduler scheduler = AndroidSchedulers.mainThread(); public void bar(Observable obs) { obs.subscribeOn(scheduler); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.java:10: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(scheduler); ~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.java line 10: Replace with observeOn(): @@ -10 +10 - obs.subscribeOn(scheduler); + obs.observeOn(scheduler); """ .trimIndent() ) } @Test fun subscribeOnMain_outsideAssignment_local_fails_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; public class Foo { public void bar(Observable obs) { Scheduler scheduler = AndroidSchedulers.mainThread(); obs.subscribeOn(scheduler); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.java:10: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(scheduler); ~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.java line 10: Replace with observeOn(): @@ -10 +10 - obs.subscribeOn(scheduler); + obs.observeOn(scheduler); """ .trimIndent() ) } @Test fun subscribeOnMain_outsideAssignment_fails_field_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { private val scheduler = AndroidSchedulers.mainThread() fun bar(obs: Observable) { obs.subscribeOn(scheduler) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:10: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(scheduler) ~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 10: Replace with observeOn(): @@ -10 +10 - obs.subscribeOn(scheduler) + obs.observeOn(scheduler) """ .trimIndent() ) } @Test fun subscribeOnMain_outsideAssignment_fails_local_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { fun bar(obs: Observable) { val scheduler = AndroidSchedulers.mainThread() obs.subscribeOn(scheduler) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:10: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] obs.subscribeOn(scheduler) ~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 10: Replace with observeOn(): @@ -10 +10 - obs.subscribeOn(scheduler) + obs.observeOn(scheduler) """ .trimIndent() ) } @Test fun subscribeOnIo_passes_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.schedulers.Schedulers; public class Foo { public void bar(Observable obs) { obs.subscribeOn(Schedulers.io()); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnIo_passes_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers object Foo { fun bar(obs: Observable) { obs.subscribeOn(Schedulers.io()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnIo_staticImport_passes_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.schedulers.Schedulers.io; public class Foo { public void bar(Observable obs) { obs.subscribeOn(io()); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .allowCompilationErrors() // Until AGP 7.1.0 // https://groups.google.com/g/lint-dev/c/BigCO8sMhKU .run() .expectClean() } @Test fun subscribeOnIo_staticImport_passes_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers.io object Foo { fun bar(obs: Observable) { obs.subscribeOn(io()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnIo_outsideAssignment_passes_java() { lint() .files( rxJavaJar3, androidSchedulers, java( """ package com.slack.lint; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.schedulers.Schedulers; public class Foo { private final Scheduler scheduler = Schedulers.io(); public void bar(Observable obs) { obs.subscribeOn(scheduler); } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnIo_outsideAssignment_passes_kotlin() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers object Foo { private val scheduler: Scheduler = Schedulers.io() fun bar(obs: Observable) { obs.subscribeOn(scheduler) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnMain_flowable_fails() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { fun bar(flow: Flowable) { flow.subscribeOn(AndroidSchedulers.mainThread()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] flow.subscribeOn(AndroidSchedulers.mainThread()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 8: Replace with observeOn(): @@ -8 +8 - flow.subscribeOn(AndroidSchedulers.mainThread()) + flow.observeOn(AndroidSchedulers.mainThread()) """ .trimIndent() ) } @Test fun subscribeOnIo_flowable_passes() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.schedulers.Schedulers object Foo { fun bar(flow: Flowable) { flow.subscribeOn(Schedulers.io()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnMain_maybe_fails() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { fun bar(maybe: Maybe) { maybe.subscribeOn(AndroidSchedulers.mainThread()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] maybe.subscribeOn(AndroidSchedulers.mainThread()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 8: Replace with observeOn(): @@ -8 +8 - maybe.subscribeOn(AndroidSchedulers.mainThread()) + maybe.observeOn(AndroidSchedulers.mainThread()) """ .trimIndent() ) } @Test fun subscribeOnIo_maybe_passes() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.schedulers.Schedulers object Foo { fun bar(maybe: Maybe) { maybe.subscribeOn(Schedulers.io()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnMain_single_fails() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { fun bar(single: Single) { single.subscribeOn(AndroidSchedulers.mainThread()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] single.subscribeOn(AndroidSchedulers.mainThread()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 8: Replace with observeOn(): @@ -8 +8 - single.subscribeOn(AndroidSchedulers.mainThread()) + single.observeOn(AndroidSchedulers.mainThread()) """ .trimIndent() ) } @Test fun subscribeOnIo_single_passes() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers object Foo { fun bar(single: Single) { single.subscribeOn(Schedulers.io()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expectClean() } @Test fun subscribeOnMain_completable_fails() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers object Foo { fun bar(completable: Completable) { completable.subscribeOn(AndroidSchedulers.mainThread()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .run() .expect( """ src/com/slack/lint/Foo.kt:8: Error: This will make the code for the initial subscription (above this line) run on the main thread. You probably want observeOn(AndroidSchedulers.mainThread()). [SubscribeOnMain] completable.subscribeOn(AndroidSchedulers.mainThread()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/com/slack/lint/Foo.kt line 8: Replace with observeOn(): @@ -8 +8 - completable.subscribeOn(AndroidSchedulers.mainThread()) + completable.observeOn(AndroidSchedulers.mainThread()) """ .trimIndent() ) } @Test fun subscribeOnIo_completable_passes() { lint() .files( rxJavaJar3, androidSchedulers, kotlin( """ package com.slack.lint import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.schedulers.Schedulers object Foo { fun bar(completable: Completable) { completable.subscribeOn(Schedulers.io()) } } """ ) .indented(), ) .issues(RxSubscribeOnMainDetector.ISSUE) .allowCompilationErrors() // Until AGP 7.1.0 // https://groups.google.com/g/lint-dev/c/BigCO8sMhKU .run() .expectClean() } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/text/SpanMarkPointMissingMaskDetectorTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.text import com.android.tools.lint.checks.infrastructure.TestMode import org.junit.Test import slack.lint.BaseSlackLintTest /** Tests [SpanMarkPointMissingMaskDetector]. */ class SpanMarkPointMissingMaskDetectorTest : BaseSlackLintTest() { override val skipTestModes: Array = arrayOf(TestMode.PARENTHESIZED) override fun getDetector() = SpanMarkPointMissingMaskDetector() override fun getIssues() = listOf(SpanMarkPointMissingMaskDetector.ISSUE) @Test fun `conforming expression - has clean report`() { val testFile = kotlin( """ package slack.text import android.text.Spanned class MyClass { fun doCheckCorrectly(spanned: Spanned): Boolean { return spanned.getSpanFlags(Object()) and Spanned.SPAN_POINT_MARK_MASK == Spanned.SPAN_INCLUSIVE_INCLUSIVE } } """ ) .indented() lint().files(testFile).issues(SpanMarkPointMissingMaskDetector.ISSUE).run().expectClean() } @Test fun `violating expression with INCLUSIVE_INCLUSIVE on left - creates error and fix`() { testViolatingExpressionLeft("Spanned.SPAN_INCLUSIVE_INCLUSIVE") } @Test fun `violating expression with INCLUSIVE_EXCLUSIVE on left - creates error and fix`() { testViolatingExpressionLeft("Spanned.SPAN_INCLUSIVE_EXCLUSIVE") } @Test fun `violating expression with EXCLUSIVE_INCLUSIVE on left - creates error and fix`() { testViolatingExpressionLeft("Spanned.SPAN_EXCLUSIVE_INCLUSIVE") } @Test fun `violating expression with EXCLUSIVE_EXCLUSIVE on left - creates error and fix`() { testViolatingExpressionLeft("Spanned.SPAN_EXCLUSIVE_EXCLUSIVE") } private fun testViolatingExpressionLeft(markPoint: String) { val testFile = kotlin( """ package slack.text import android.text.Spanned class MyClass { fun doCheckIncorrectly(spanned: Spanned): Boolean { return spanned.getSpanFlags(Object()) == $markPoint || Spanned.x() } } """ ) .indented() lint() .files(testFile) .issues(SpanMarkPointMissingMaskDetector.ISSUE) .run() .expect( """ src/slack/text/MyClass.kt:7: Error: Do not check against $markPoint directly. Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. [SpanMarkPointMissingMask] return spanned.getSpanFlags(Object()) == $markPoint || Spanned.x() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/text/MyClass.kt line 7: Use bitwise mask: @@ -7 +7 - return spanned.getSpanFlags(Object()) == $markPoint || Spanned.x() + return ((spanned.getSpanFlags(Object())) and android.text.Spanned.SPAN_POINT_MARK_MASK) == $markPoint || Spanned.x() """ .trimIndent() ) } @Test fun `violating expression with INCLUSIVE_INCLUSIVE on right - creates error and fix`() { testViolatingExpressionRight("SPAN_INCLUSIVE_INCLUSIVE") } @Test fun `violating expression with INCLUSIVE_EXCLUSIVE on right - creates error and fix`() { testViolatingExpressionRight("SPAN_INCLUSIVE_EXCLUSIVE") } @Test fun `violating expression with EXCLUSIVE_INCLUSIVE on right - creates error and fix`() { testViolatingExpressionRight("SPAN_EXCLUSIVE_INCLUSIVE") } @Test fun `violating expression with EXCLUSIVE_EXCLUSIVE on right - creates error and fix`() { testViolatingExpressionRight("SPAN_EXCLUSIVE_EXCLUSIVE") } private fun testViolatingExpressionRight(markPoint: String) { val testFile = kotlin( """ package slack.text import android.text.Spanned.$markPoint class MyClass { fun doCheckIncorrectly(spanned: Spanned): Boolean { return $markPoint == spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() } } """ ) .indented() lint() .files(testFile) .issues(SpanMarkPointMissingMaskDetector.ISSUE) .run() .expect( """ src/slack/text/MyClass.kt:7: Error: Do not check against $markPoint directly. Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. [SpanMarkPointMissingMask] return $markPoint == spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/text/MyClass.kt line 7: Use bitwise mask: @@ -7 +7 - return $markPoint == spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() + return $markPoint == ((spanned.getSpanFlags(Object())) and android.text.Spanned.SPAN_POINT_MARK_MASK) || isBoolean1() && isBoolean2() """ .trimIndent() ) } @Test fun `violating expression with fully qualified - creates error and fix`() { val markPoint = "android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE" val testFile = kotlin( """ package slack.text class MyClass { fun doCheckIncorrectly(spanned: Spanned): Boolean { return $markPoint == spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() } } """ ) .indented() lint() .files(testFile) .issues(SpanMarkPointMissingMaskDetector.ISSUE) .run() .expect( """ src/slack/text/MyClass.kt:5: Error: Do not check against android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE directly. Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. [SpanMarkPointMissingMask] return $markPoint == spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/text/MyClass.kt line 5: Use bitwise mask: @@ -5 +5 - return $markPoint == spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() + return $markPoint == ((spanned.getSpanFlags(Object())) and android.text.Spanned.SPAN_POINT_MARK_MASK) || isBoolean1() && isBoolean2() """ .trimIndent() ) } @Test fun `violating expression with not equal - creates error and fix`() { val markPoint = "android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE" val testFile = kotlin( """ package slack.text class MyClass { fun doCheckIncorrectly(spanned: Spanned): Boolean { return $markPoint != spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() } } """ ) .indented() lint() .files(testFile) .issues(SpanMarkPointMissingMaskDetector.ISSUE) .run() .expect( """ src/slack/text/MyClass.kt:5: Error: Do not check against android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE directly. Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. [SpanMarkPointMissingMask] return $markPoint != spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/text/MyClass.kt line 5: Use bitwise mask: @@ -5 +5 - return $markPoint != spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() + return $markPoint != ((spanned.getSpanFlags(Object())) and android.text.Spanned.SPAN_POINT_MARK_MASK) || isBoolean1() && isBoolean2() """ .trimIndent() ) } @Test fun `violating expression with identity equality- creates error and fix`() { val markPoint = "android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE" val testFile = kotlin( """ package slack.text class MyClass { fun doCheckIncorrectly(spanned: Spanned): Boolean { return $markPoint === spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() } } """ ) .indented() lint() .files(testFile) .issues(SpanMarkPointMissingMaskDetector.ISSUE) .run() .expect( """ src/slack/text/MyClass.kt:5: Error: Do not check against android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE directly. Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. [SpanMarkPointMissingMask] return $markPoint === spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/text/MyClass.kt line 5: Use bitwise mask: @@ -5 +5 - return $markPoint === spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() + return $markPoint === ((spanned.getSpanFlags(Object())) and android.text.Spanned.SPAN_POINT_MARK_MASK) || isBoolean1() && isBoolean2() """ .trimIndent() ) } @Test fun `violating expression with not identity equality - creates error and fix`() { val markPoint = "android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE" val testFile = kotlin( """ package slack.text class MyClass { fun doCheckIncorrectly(spanned: Spanned): Boolean { return $markPoint !== spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() } } """ ) .indented() lint() .files(testFile) .issues(SpanMarkPointMissingMaskDetector.ISSUE) .run() .expect( """ src/slack/text/MyClass.kt:5: Error: Do not check against android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE directly. Instead mask flag with Spanned.SPAN_POINT_MARK_MASK to only check MARK_POINT flags. [SpanMarkPointMissingMask] return $markPoint !== spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 errors, 0 warnings """ .trimIndent() ) .expectFixDiffs( """ Fix for src/slack/text/MyClass.kt line 5: Use bitwise mask: @@ -5 +5 - return $markPoint !== spanned.getSpanFlags(Object()) || isBoolean1() && isBoolean2() + return $markPoint !== ((spanned.getSpanFlags(Object())) and android.text.Spanned.SPAN_POINT_MARK_MASK) || isBoolean1() && isBoolean2() """ .trimIndent() ) } } ================================================ FILE: slack-lint-checks/src/test/java/slack/lint/util/LintUtilsTest.kt ================================================ // Copyright (C) 2021 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 package slack.lint.util import com.google.common.truth.Truth.assertThat import org.junit.Test class LintUtilsTest { @Test fun snakeToCamelTests() { "noChange" assertSnakeToCamel "noChange" "NoChange" assertSnakeToCamel "NoChange" "test-value" assertSnakeToCamel "testValue" "test_value" assertSnakeToCamel "testValue" "test_value_multiple" assertSnakeToCamel "testValueMultiple" "multi__scores" assertSnakeToCamel "multiScores" "trailing__" assertSnakeToCamel "trailing" "___leading" assertSnakeToCamel "leading" } @Test fun toScreamingSnakeTests() { "camelCase" assertCamelToScreamingSnake "CAMEL_CASE" "CapCamelCase" assertCamelToScreamingSnake "CAP_CAMEL_CASE" "NO_CHANGE" assertCamelToScreamingSnake "NO_CHANGE" "test-value" assertCamelToScreamingSnake "TEST_VALUE" "test_value" assertCamelToScreamingSnake "TEST_VALUE" "test_value_multiple" assertCamelToScreamingSnake "TEST_VALUE_MULTIPLE" "multi__scores" assertCamelToScreamingSnake "MULTI_SCORES" "trailing__" assertCamelToScreamingSnake "TRAILING" "___leading" assertCamelToScreamingSnake "LEADING" } private infix fun String.assertSnakeToCamel(other: String) { assertThat(this.snakeToCamel()).isEqualTo(other) } private infix fun String.assertCamelToScreamingSnake(other: String) { assertThat(this.toScreamingSnakeCase()).isEqualTo(other) } } ================================================ FILE: slack-lint-checks/src/test/resources/com/slack/lint/data/testStubs/ViewContextDetectorTestContentProvider.java ================================================ package foo; import android.content.ContentProvider; import android.content.Context; public abstract class DummyProvider extends ContentProvider { public void bar() { Context c = getContext(); } } ================================================ FILE: slack-lint-checks/src/test/resources/com/slack/lint/data/testStubs/ViewContextDetectorTestCustomViewInternalCaller.java ================================================ package foo; import android.app.Activity; import android.content.Context; import android.util.AttributeSet; import android.widget.TextView; public class Example extends TextView { public Example(Context context, AttributeSet attrs) { super(context, attrs); } public void bar() { Activity a = (Activity) getContext(); } } ================================================ FILE: slack-lint-checks/src/test/resources/com/slack/lint/data/testStubs/ViewContextDetectorTestExternalCallerOnCustomView.java ================================================ package foo; import android.app.Activity; import android.content.Context; import android.util.AttributeSet; import android.widget.TextView; public class Example { TextView view; public Example(TextView v) { view = v; } public void bar() { Activity activity = (Activity) view.getContext(); } } ================================================ FILE: slack-lint-checks/src/test/resources/com/slack/lint/data/testStubs/ViewContextDetectorTestExternalCallerOnView.java ================================================ package foo; import android.app.Activity; import android.content.Context; import android.util.AttributeSet; import android.view.View; public class Example { View view; public Example(View v) { view = v; } public void bar() { Activity activity = (Activity) view.getContext(); } } ================================================ FILE: spotless/spotless.kt ================================================ // Copyright (C) $YEAR Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0