├── .editorconfig ├── .github ├── ci-gradle.properties ├── release-drafter.yml └── workflows │ ├── build.yaml │ ├── create-release.yaml │ ├── publish-docs.yaml │ └── update-release.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASING.md ├── art └── logo.webp ├── build.gradle.kts ├── docs ├── detekt.md ├── index.md ├── ktlint.md └── rules.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mkdocs.yml ├── renovate.json ├── rules ├── common │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ └── main │ │ └── kotlin │ │ └── io │ │ └── nlopez │ │ └── compose │ │ ├── core │ │ ├── ComposeKtConfig.kt │ │ ├── ComposeKtVisitor.kt │ │ ├── Emitter.kt │ │ └── util │ │ │ ├── ASTNodes.kt │ │ │ ├── Composables.kt │ │ │ ├── KotlinUtils.kt │ │ │ ├── KtAnnotateds.kt │ │ │ ├── KtCallExpressions.kt │ │ │ ├── KtCallableDeclarations.kt │ │ │ ├── KtConstantExpressions.kt │ │ │ ├── KtDotQualifiedExpressions.kt │ │ │ ├── KtFunctions.kt │ │ │ ├── KtImportLists.kt │ │ │ ├── KtParameters.kt │ │ │ ├── Lambdas.kt │ │ │ ├── Modifiers.kt │ │ │ ├── Previews.kt │ │ │ └── PsiElements.kt │ │ └── rules │ │ ├── ComposableAnnotationNaming.kt │ │ ├── CompositionLocalAllowlist.kt │ │ ├── CompositionLocalNaming.kt │ │ ├── ContentEmitterReturningValues.kt │ │ ├── ContentSlotReused.kt │ │ ├── ContentTrailingLambda.kt │ │ ├── DefaultsVisibility.kt │ │ ├── LambdaParameterEventTrailing.kt │ │ ├── LambdaParameterInRestartableEffect.kt │ │ ├── Material2.kt │ │ ├── ModifierClickableOrder.kt │ │ ├── ModifierComposed.kt │ │ ├── ModifierMissing.kt │ │ ├── ModifierNaming.kt │ │ ├── ModifierNotUsedAtRoot.kt │ │ ├── ModifierReused.kt │ │ ├── ModifierWithoutDefault.kt │ │ ├── MultipleContentEmitters.kt │ │ ├── MutableParameters.kt │ │ ├── MutableStateAutoboxing.kt │ │ ├── MutableStateParameter.kt │ │ ├── Naming.kt │ │ ├── ParameterNaming.kt │ │ ├── ParameterOrder.kt │ │ ├── PreviewAnnotationNaming.kt │ │ ├── PreviewNaming.kt │ │ ├── PreviewPublic.kt │ │ ├── RememberContentMissing.kt │ │ ├── RememberStateMissing.kt │ │ ├── UnstableCollections.kt │ │ ├── ViewModelForwarding.kt │ │ └── ViewModelInjection.kt ├── detekt │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ ├── main │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── nlopez │ │ │ │ └── compose │ │ │ │ └── rules │ │ │ │ ├── DetektComposeKtConfig.kt │ │ │ │ ├── DetektRule.kt │ │ │ │ └── detekt │ │ │ │ ├── ComposableAnnotationNamingCheck.kt │ │ │ │ ├── ComposeRuleSetProvider.kt │ │ │ │ ├── CompositionLocalAllowlistCheck.kt │ │ │ │ ├── CompositionLocalNamingCheck.kt │ │ │ │ ├── ContentEmitterReturningValuesCheck.kt │ │ │ │ ├── ContentSlotReusedCheck.kt │ │ │ │ ├── ContentTrailingLambdaCheck.kt │ │ │ │ ├── DefaultsVisibilityCheck.kt │ │ │ │ ├── LambdaParameterEventTrailingCheck.kt │ │ │ │ ├── LambdaParameterInRestartableEffectCheck.kt │ │ │ │ ├── Material2Check.kt │ │ │ │ ├── ModifierClickableOrderCheck.kt │ │ │ │ ├── ModifierComposedCheck.kt │ │ │ │ ├── ModifierMissingCheck.kt │ │ │ │ ├── ModifierNamingCheck.kt │ │ │ │ ├── ModifierNotUsedAtRootCheck.kt │ │ │ │ ├── ModifierReusedCheck.kt │ │ │ │ ├── ModifierWithoutDefaultCheck.kt │ │ │ │ ├── MultipleContentEmittersCheck.kt │ │ │ │ ├── MutableParametersCheck.kt │ │ │ │ ├── MutableStateAutoboxingCheck.kt │ │ │ │ ├── MutableStateParameterCheck.kt │ │ │ │ ├── NamingCheck.kt │ │ │ │ ├── ParameterNamingCheck.kt │ │ │ │ ├── ParameterOrderCheck.kt │ │ │ │ ├── PreviewAnnotationNamingCheck.kt │ │ │ │ ├── PreviewNamingCheck.kt │ │ │ │ ├── PreviewPublicCheck.kt │ │ │ │ ├── RememberContentMissingCheck.kt │ │ │ │ ├── RememberStateMissingCheck.kt │ │ │ │ ├── UnstableCollectionsCheck.kt │ │ │ │ ├── ViewModelForwardingCheck.kt │ │ │ │ └── ViewModelInjectionCheck.kt │ │ └── resources │ │ │ ├── META-INF │ │ │ └── services │ │ │ │ └── io.gitlab.arturbosch.detekt.api.RuleSetProvider │ │ │ └── config │ │ │ └── config.yml │ │ └── test │ │ └── kotlin │ │ └── io │ │ └── nlopez │ │ └── compose │ │ └── rules │ │ ├── DetektComposeKtConfigTest.kt │ │ └── detekt │ │ ├── ComposableAnnotationNamingCheckTest.kt │ │ ├── ComposeRuleSetProviderTest.kt │ │ ├── CompositionLocalAllowlistCheckTest.kt │ │ ├── CompositionLocalNamingCheckTest.kt │ │ ├── ContentEmitterReturningValuesCheckTest.kt │ │ ├── ContentSlotReusedCheckTest.kt │ │ ├── ContentTrailingLambdaCheckTest.kt │ │ ├── DefaultsVisibilityCheckTest.kt │ │ ├── LambdaParameterEventTrailingCheckTest.kt │ │ ├── LambdaParameterInRestartableEffectCheckTest.kt │ │ ├── Material2CheckTest.kt │ │ ├── ModifierClickableOrderCheckTest.kt │ │ ├── ModifierComposedCheckTest.kt │ │ ├── ModifierMissingCheckTest.kt │ │ ├── ModifierNamingCheckTest.kt │ │ ├── ModifierNotUsedAtRootCheckTest.kt │ │ ├── ModifierReusedCheckTest.kt │ │ ├── ModifierWithoutDefaultCheckTest.kt │ │ ├── MultipleContentEmittersCheckTest.kt │ │ ├── MutableParametersCheckTest.kt │ │ ├── MutableStateAutoboxingCheckTest.kt │ │ ├── MutableStateParameterCheckTest.kt │ │ ├── NamingCheckTest.kt │ │ ├── ParameterNamingCheckTest.kt │ │ ├── ParameterOrderCheckTest.kt │ │ ├── PreviewAnnotationNamingCheckTest.kt │ │ ├── PreviewNamingCheckTest.kt │ │ ├── PreviewPublicCheckTest.kt │ │ ├── RememberContentMissingCheckTest.kt │ │ ├── RememberStateMissingCheckTest.kt │ │ ├── UnstableCollectionsCheckTest.kt │ │ ├── ViewModelForwardingCheckTest.kt │ │ └── ViewModelInjectionCheckTest.kt └── ktlint │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── nlopez │ │ │ └── compose │ │ │ └── rules │ │ │ ├── KtlintComposeKtConfig.kt │ │ │ ├── KtlintRule.kt │ │ │ └── ktlint │ │ │ ├── ComposableAnnotationNamingCheck.kt │ │ │ ├── ComposeRuleSetProvider.kt │ │ │ ├── CompositionLocalAllowlistCheck.kt │ │ │ ├── CompositionLocalNamingCheck.kt │ │ │ ├── ContentEmitterReturningValuesCheck.kt │ │ │ ├── ContentSlotReusedCheck.kt │ │ │ ├── ContentTrailingLambdaCheck.kt │ │ │ ├── DefaultsVisibilityCheck.kt │ │ │ ├── EditorConfigProperties.kt │ │ │ ├── LambdaParameterEventTrailingCheck.kt │ │ │ ├── LambdaParameterInRestartableEffectCheck.kt │ │ │ ├── Material2Check.kt │ │ │ ├── ModifierClickableOrderCheck.kt │ │ │ ├── ModifierComposedCheck.kt │ │ │ ├── ModifierMissingCheck.kt │ │ │ ├── ModifierNamingCheck.kt │ │ │ ├── ModifierNotUsedAtRootCheck.kt │ │ │ ├── ModifierReusedCheck.kt │ │ │ ├── ModifierWithoutDefaultCheck.kt │ │ │ ├── MultipleContentEmittersCheck.kt │ │ │ ├── MutableParametersCheck.kt │ │ │ ├── MutableStateAutoboxingCheck.kt │ │ │ ├── MutableStateParameterCheck.kt │ │ │ ├── NamingCheck.kt │ │ │ ├── ParameterNamingCheck.kt │ │ │ ├── ParameterOrderCheck.kt │ │ │ ├── PreviewAnnotationNamingCheck.kt │ │ │ ├── PreviewNamingCheck.kt │ │ │ ├── PreviewPublicCheck.kt │ │ │ ├── RememberContentMissingCheck.kt │ │ │ ├── RememberStateMissingCheck.kt │ │ │ ├── UnstableCollectionsCheck.kt │ │ │ ├── ViewModelForwardingCheck.kt │ │ │ └── ViewModelInjectionCheck.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ └── com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 │ └── test │ └── kotlin │ └── io │ └── nlopez │ └── compose │ └── rules │ ├── KtlintComposeKtConfigTest.kt │ └── ktlint │ ├── ComposableAnnotationNamingCheckTest.kt │ ├── ComposeRuleSetProviderTest.kt │ ├── CompositionLocalAllowlistCheckTest.kt │ ├── CompositionLocalNamingCheckTest.kt │ ├── ContentEmitterReturningValuesCheckTest.kt │ ├── ContentSlotReusedCheckTest.kt │ ├── ContentTrailingLambdaCheckTest.kt │ ├── DefaultsVisibilityCheckTest.kt │ ├── LambdaParameterInRestartableEffectCheckTest.kt │ ├── Material2CheckTest.kt │ ├── ModifierClickableOrderCheckTest.kt │ ├── ModifierComposedCheckTest.kt │ ├── ModifierMissingCheckTest.kt │ ├── ModifierNamingCheckTest.kt │ ├── ModifierNotUsedAtRootCheckTest.kt │ ├── ModifierReusedCheckTest.kt │ ├── ModifierWithoutDefaultCheckTest.kt │ ├── MultipleContentEmittersCheckTest.kt │ ├── MutableParametersCheckTest.kt │ ├── MutableStateAutoboxingCheckTest.kt │ ├── MutableStateParameterCheckTest.kt │ ├── NamingCheckTest.kt │ ├── ParameterNamingCheckTest.kt │ ├── ParameterOrderCheckTest.kt │ ├── PreviewAnnotationNamingCheckTest.kt │ ├── PreviewNamingCheckTest.kt │ ├── PreviewPublicCheckTest.kt │ ├── RememberContentMissingCheckTest.kt │ ├── RememberStateMissingCheckTest.kt │ ├── UnstableCollectionsCheckTest.kt │ ├── ViewModelForwardingCheckTest.kt │ └── ViewModelInjectionCheckTest.kt ├── scripts ├── create-rule.main.kts └── templates │ ├── DetektRule.kt.template │ ├── DetektRuleTest.kt.template │ ├── KtlintRule.kt.template │ ├── KtlintRuleTest.kt.template │ └── Rule.kt.template ├── settings.gradle.kts └── spotless └── copyright.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | max_line_length = 120 9 | tab_width = 8 10 | ij_continuation_indent_size = 8 11 | 12 | [*.{kt,kts}] 13 | ij_continuation_indent_size = 4 14 | ij_kotlin_name_count_to_use_star_import = 2147483647 15 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 16 | ktlint_code_style = intellij_idea 17 | ktlint_function_naming_ignore_when_annotated_with = Composable 18 | ktlint_standard_discouraged-comment-location = disabled 19 | 20 | [{*.gant,*.gradle,*.groovy,*.gy}] 21 | ij_groovy_keep_control_statement_in_one_line = false 22 | ij_groovy_align_multiline_for = false 23 | ij_groovy_align_multiline_parameters = false 24 | ij_groovy_call_parameters_wrap = normal 25 | ij_groovy_method_parameters_wrap = normal 26 | ij_groovy_extends_list_wrap = normal 27 | ij_groovy_throws_list_wrap = normal 28 | ij_groovy_extends_keyword_wrap = normal 29 | ij_groovy_throws_keyword_wrap = normal 30 | ij_groovy_method_call_chain_wrap = normal 31 | ij_groovy_binary_operation_wrap = normal 32 | ij_groovy_ternary_operation_wrap = normal 33 | ij_groovy_for_statement_wrap = normal 34 | ij_groovy_array_initializer_wrap = normal 35 | ij_groovy_assignment_wrap = normal 36 | ij_groovy_if_brace_force = always 37 | ij_groovy_do_while_brace_force = always 38 | ij_groovy_while_brace_force = always 39 | ij_groovy_for_brace_force = always 40 | 41 | [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] 42 | ij_continuation_indent_size = 4 43 | 44 | [*.{yml,yaml}] 45 | indent_size = 2 46 | -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=1536m -XX:+HeapDumpOnOutOfMemoryError 3 | org.gradle.workers.max=2 4 | 5 | kotlin.compiler.execution.strategy=in-process 6 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | template: | 4 | ## What's changed 5 | 6 | $CHANGES 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build & test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | - 'docs/**' 10 | pull_request: 11 | paths-ignore: 12 | - '**.md' 13 | - 'docs/**' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 30 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup JDK 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: 'zulu' 27 | java-version: 11 28 | 29 | - name: Copy CI gradle.properties 30 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 31 | 32 | - uses: gradle/actions/setup-gradle@v4 33 | 34 | - name: Build 35 | run: ./gradlew spotlessCheck assemble test 36 | 37 | - name: Upload test results 38 | if: always() 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: test-results 42 | path: | 43 | **/build/test-results/* 44 | **/build/reports/* 45 | 46 | deploy: 47 | if: github.event_name == 'push' && github.repository == 'mrmans0n/compose-rules' 48 | runs-on: ubuntu-latest 49 | needs: [ build ] 50 | timeout-minutes: 30 51 | env: 52 | TERM: dumb 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | 57 | - name: Copy CI gradle.properties 58 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 59 | 60 | - name: Setup JDK 61 | uses: actions/setup-java@v4 62 | with: 63 | distribution: 'zulu' 64 | java-version: 11 65 | 66 | - uses: gradle/actions/setup-gradle@v4 67 | 68 | - name: Deploy to Sonatype 69 | run: ./gradlew clean publish --no-parallel --no-daemon --no-configuration-cache --stacktrace 70 | env: 71 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 72 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 73 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} 74 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} 75 | 76 | - name: Add artifact version to ENV 77 | run: echo "VERSION=$(./gradlew -q printVersion)" >> $GITHUB_ENV 78 | 79 | - name: Publish release 80 | run: ./gradlew releaseRepository 81 | if: success() && !endsWith(env.VERSION, '-SNAPSHOT') 82 | env: 83 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 84 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 85 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yaml: -------------------------------------------------------------------------------- 1 | name: Update release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_draft_release: 10 | if: github.repository == 'mrmans0n/compose-rules' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | 10 | jobs: 11 | deploy_docs: 12 | if: github.repository == 'mrmans0n/compose-rules' 13 | runs-on: ubuntu-latest 14 | env: 15 | TERM: dumb 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install dependencies 26 | run: | 27 | python3 -m pip install --upgrade pip 28 | python3 -m pip install mkdocs 29 | python3 -m pip install mkdocs-material 30 | python3 -m pip install mdx_truly_sane_lists 31 | 32 | - name: Build site 33 | run: mkdocs build 34 | 35 | - name: Deploy docs 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: ./site 40 | -------------------------------------------------------------------------------- /.github/workflows/update-release.yaml: -------------------------------------------------------------------------------- 1 | name: Update release with binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | update_release: 10 | if: github.repository == 'mrmans0n/compose-rules' 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | env: 14 | TERM: dumb 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Copy CI gradle.properties 20 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 21 | 22 | - name: Setup JDK 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: 'zulu' 26 | java-version: 11 27 | 28 | - uses: gradle/actions/setup-gradle@v4 29 | 30 | - name: Add artifact version to ENV 31 | run: echo "VERSION=$(./gradlew -q printVersion)" >> $GITHUB_ENV 32 | 33 | - name: Build the fat jars for CLI usage 34 | if: success() && !endsWith(env.VERSION, '-SNAPSHOT') 35 | run: ./gradlew clean :rules:ktlint:shadowJar :rules:detekt:shadowJar -PuberJar --rerun-tasks 36 | 37 | - name: Upload ktlint fat jars binaries to release 38 | if: success() && !endsWith(env.VERSION, '-SNAPSHOT') 39 | uses: svenstaro/upload-release-action@v2 40 | with: 41 | repo_token: ${{ secrets.GITHUB_TOKEN }} 42 | file: rules/ktlint/build/libs/ktlint-${{ env.VERSION }}-all.jar 43 | tag: v${{ env.VERSION }} 44 | asset_name: ktlint-compose-${{ env.VERSION }}-all.jar 45 | overwrite: true 46 | 47 | - name: Upload detekt fat jars binaries to release 48 | if: success() && !endsWith(env.VERSION, '-SNAPSHOT') 49 | uses: svenstaro/upload-release-action@v2 50 | with: 51 | repo_token: ${{ secrets.GITHUB_TOKEN }} 52 | file: rules/detekt/build/libs/detekt-${{ env.VERSION }}-all.jar 53 | tag: v${{ env.VERSION }} 54 | asset_name: detekt-compose-${{ env.VERSION }}-all.jar 55 | overwrite: true 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | 35 | # Banish DS_Store files 36 | .DS_Store 37 | 38 | # MkDocs site 39 | site/ 40 | 41 | # Fleet 42 | **/.fleet/settings.json 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to get patches from you! 4 | 5 | ## Building the Project 6 | 7 | ```shell 8 | ./gradlew build 9 | ``` 10 | 11 | Running the build task will make sure to compile the code, run all the tests and pass all the linters necessary. 12 | 13 | ## Workflow 14 | 15 | 1. Fork the repo 16 | 2. Create a feature branch 17 | 3. Write code and tests for your change 18 | 4. Make sure the code builds and all the linters pass before submitting your changes. 19 | 5. From your branch, make a pull request against the main repo (`mrmans0n/compose-rules`) 20 | 6. Work with the repo maintainers to get your change reviewed 21 | 7. Wait for your change to get merged into the `main` branch in the main repo. 22 | 23 | # Bug reports 24 | 25 | A bug is a demonstrable problem that is caused by the code in the repository. Good bug reports are extremely helpful - thank you! 26 | 27 | 1. Use the GitHub issue search — check if the issue has already been reported. 28 | 2. Check if the issue has been fixed — try to reproduce it using the latest main branch in the repository. 29 | 3. Isolate the problem — ideally create a reduced test case and a live example. 30 | 4. Please try to be as detailed as possible in your report. Include specific information about the environment - Kotlin version, Jetpack Compose version and steps required to reproduce the issue. 31 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update the `VERSION_NAME` in `gradle.properties` to the release version (e.g. remove `-SNAPSHOT` from it). 4 | 5 | 2. Commit and push to main 6 | 7 | ``` 8 | $ git commit -am "Bump version to X.Y.Z" 9 | $ git push 10 | ``` 11 | This will trigger a GitHub Action workflow that will publish the artifacts to Maven Central, and publish them. 12 | 3. Go to [Releases](https://github.com/mrmans0n/compose-rules/releases) and you'll see a draft release with all the PRs listed in the changelog. 13 | 1. Make sure the release name and the tags associated to it match. If not, you make them match. 14 | 2. Publish the draft of the release. 15 | 16 | 4. Update the `VERSION_NAME` in `gradle.properties` to the next SNAPSHOT version by adding `-SNAPSHOT` to the version. 17 | 5. Commit and push to main 18 | 19 | ``` 20 | $ git commit -am "Bump version to X.Y.Z-SNAPSHOT" 21 | $ git push 22 | ``` 23 | 6. You're done! 🎉 24 | -------------------------------------------------------------------------------- /art/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmans0n/compose-rules/3636d61858e6d498e56416a1efe90460c425736a/art/logo.webp -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | import com.diffplug.gradle.spotless.SpotlessExtension 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.jvm) apply false 8 | alias(libs.plugins.mavenPublish) apply false 9 | alias(libs.plugins.spotless) apply false 10 | } 11 | 12 | allprojects { 13 | val libs = rootProject.libs 14 | 15 | pluginManager.apply(libs.plugins.spotless.get().pluginId) 16 | configure { 17 | val ktlintVersion = libs.versions.ktlint.get() 18 | kotlin { 19 | target("**/*.kt") 20 | ktlint(ktlintVersion) 21 | 22 | licenseHeaderFile(rootProject.file("spotless/copyright.txt")) 23 | } 24 | kotlinGradle { 25 | target("*.kts") 26 | ktlint(ktlintVersion) 27 | } 28 | } 29 | 30 | plugins.withType { 31 | extensions.configure { 32 | toolchain { 33 | languageVersion.set(JavaLanguageVersion.of(11)) 34 | } 35 | } 36 | } 37 | 38 | tasks.withType().configureEach { 39 | useJUnitPlatform() 40 | } 41 | 42 | tasks.withType().configureEach { 43 | compilerOptions { 44 | // Treat all Kotlin warnings as errors 45 | allWarningsAsErrors = true 46 | freeCompilerArgs.addAll( 47 | // Enable default methods in interfaces 48 | "-Xjvm-default=all", 49 | ) 50 | } 51 | } 52 | 53 | version = project.property("VERSION_NAME") ?: "0.0.0" 54 | if (project != rootProject) { 55 | pluginManager.apply(libs.plugins.mavenPublish.get().pluginId) 56 | } 57 | 58 | project.configurations.create("compileOnlyOrApi") { 59 | isCanBeConsumed = false 60 | isCanBeResolved = true 61 | 62 | project.configurations.configureEach { 63 | when { 64 | name == "api" && project.hasProperty("uberJar") -> extendsFrom(this@create) 65 | name == "compileOnly" -> extendsFrom(this@create) 66 | name == "testImplementation" -> extendsFrom(this@create) 67 | } 68 | } 69 | } 70 | } 71 | 72 | tasks.register("printVersion") { 73 | doLast { 74 | println(project.property("VERSION_NAME")) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | The Compose Rules is a set of custom Ktlint / Detekt rules to ensure that your composables don't fall into common pitfalls, that might be easy to miss in code reviews. 2 | 3 | ## Why 4 | It can be challenging for big teams to start adopting Compose, particularly because not everyone will start at same time or with the same patterns. We tried to ease the pain by creating a set of Compose static checks. 5 | 6 | Compose has lots of superpowers but also has a bunch of footguns to be aware of [as seen in this Twitter Thread](https://twitter.com/mrmans0n/status/1507390768796909571). 7 | 8 | This is where our static checks come in. We want to detect as many potential issues as we can, as quickly as we can. In this case we want an error to show prior to engineers having to review code. Similar to other static check libraries we hope this leads to a "don't shoot the messengers" philosphy which will foster healthy Compose adoption. 9 | 10 | ## Using with ktlint 11 | 12 | You can refer to the [Using with ktlint](https://mrmans0n.github.io/compose-rules/ktlint) documentation. 13 | 14 | ## Using with detekt 15 | 16 | You can refer to the [Using with detekt](https://mrmans0n.github.io/compose-rules/detekt) documentation. 17 | 18 | ## Migrating from Twitter Compose Rules 19 | 20 | The process to migrate to these rules coming from the Twitter ones is simple. 21 | 22 | 1. Change the project coordinates in your gradle build scripts 23 | 2. For detekt, `com.twitter.compose.rules:detekt:$version` becomes `io.nlopez.compose.rules:detekt:$version` 24 | 3. For ktlint, `com.twitter.compose.rules:ktlint:$version` becomes `io.nlopez.compose.rules:ktlint:$version` 25 | 4. Update `$version` to the latest: ![Latest version](https://img.shields.io/maven-central/v/io.nlopez.compose.rules/common) - see the project [releases page](https://github.com/mrmans0n/compose-rules/releases). 26 | 5. **If you are using Detekt**: update the config file (e.g. `detekt.yml`) so that the rule set name `TwitterCompose` becomes `Compose`. Keep in mind that there are a lot of new rules in this repo that weren't in Twitter's, so you'd be better copying over from the [example configuration](https://mrmans0n.github.io/compose-rules/detekt). 27 | 6. Done! 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Turn on parallel compilation, caching and on-demand configuration 2 | org.gradle.caching=true 3 | org.gradle.parallel=true 4 | org.gradle.configuration-cache.parallel=true 5 | # Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308) 6 | systemProp.org.gradle.internal.publish.checksums.insecure=true 7 | # Increase timeout when pushing to Sonatype 8 | systemProp.org.gradle.internal.http.socketTimeout=240000 9 | 10 | # POM 11 | SONATYPE_HOST=DEFAULT 12 | RELEASE_SIGNING_ENABLED=true 13 | GROUP=io.nlopez.compose.rules 14 | VERSION_NAME=0.4.23-SNAPSHOT 15 | POM_DESCRIPTION=Jetpack Compose Rules 16 | POM_INCEPTION_YEAR=2022 17 | POM_URL=https://github.com/mrmans0n/compose-rules/ 18 | POM_SCM_URL=https://github.com/mrmans0n/compose-rules/ 19 | POM_SCM_CONNECTION=scm:git:git://github.com/mrmans0n/compose-rules.git 20 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/mrmans0n/compose-rules.git 21 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 22 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 23 | POM_LICENCE_DIST=repo 24 | POM_DEVELOPER_ID=mrmans0n 25 | POM_DEVELOPER_NAME=Nacho Lopez 26 | POM_DEVELOPER_URL=https://github.com/mrmans0n/ 27 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.20" 3 | ktlint = "1.5.0" 4 | detekt = "1.23.8" 5 | junit = "5.11.4" 6 | 7 | [libraries] 8 | ktlint-core = { module = "com.pinterest.ktlint:ktlint-core", version.ref = "ktlint" } 9 | ktlint-rule-engine-core = { module = "com.pinterest.ktlint:ktlint-rule-engine-core", version.ref = "ktlint" } 10 | ktlint-rule-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" } 11 | ktlint-cli-ruleset-core = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint" } 12 | ktlint-test = { module = "com.pinterest.ktlint:ktlint-test", version.ref = "ktlint" } 13 | 14 | detekt-core = { module = "io.gitlab.arturbosch.detekt:detekt-core", version.ref = "detekt" } 15 | detekt-test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } 16 | 17 | kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } 18 | 19 | kaml = "com.charleskorn.kaml:kaml:0.80.1" 20 | 21 | junit5 = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } 22 | junit5-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } 23 | assertj = "org.assertj:assertj-core:3.27.3" 24 | konsist = "com.lemonappdev:konsist:0.17.3" 25 | reflections = "org.reflections:reflections:0.10.2" 26 | 27 | [plugins] 28 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 29 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 30 | mavenPublish = "com.vanniktech.maven.publish:0.32.0" 31 | spotless = { id = "com.diffplug.spotless", version = "7.0.4" } 32 | shadowJar = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } 33 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrmans0n/compose-rules/3636d61858e6d498e56416a1efe90460c425736a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 'Jetpack Compose Rules' 2 | site_description: 'Compose Rules is a set of custom ktlint and detekt rules to ensure that your composables do not fall into common pitfalls' 3 | site_author: 'Nacho Lopez' 4 | site_url: 'https://mrmans0h.github.io/compose-rules/' 5 | edit_uri: 'tree/main/docs/' 6 | remote_branch: gh-pages 7 | 8 | docs_dir: docs 9 | 10 | repo_name: 'Compose Rules' 11 | repo_url: 'https://github.com/mrmans0n/compose-rules' 12 | 13 | # Navigation 14 | nav: 15 | - 'Overview': index.md 16 | - 'Using with ktlint': ktlint.md 17 | - 'Using with detekt': detekt.md 18 | - 'Rules': rules.md 19 | 20 | # Configuration 21 | theme: 22 | name: 'material' 23 | language: 'en' 24 | palette: 25 | # Palette toggle for light mode 26 | - scheme: default 27 | primary: 'deep orange' 28 | accent: 'red' 29 | toggle: 30 | icon: material/brightness-7 31 | name: Switch to dark mode 32 | # Palette toggle for dark mode 33 | - scheme: slate 34 | primary: 'deep orange' 35 | accent: 'red' 36 | toggle: 37 | icon: material/brightness-4 38 | name: Switch to light mode 39 | font: 40 | text: 'Roboto' 41 | code: 'JetBrains Mono' 42 | icon: 43 | logo: material/ruler-square 44 | 45 | # Extensions 46 | markdown_extensions: 47 | - admonition 48 | - attr_list 49 | - pymdownx.emoji: 50 | emoji_index: !!python/name:material.extensions.emoji.twemoji 51 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 52 | - codehilite: 53 | guess_lang: false 54 | - footnotes 55 | - toc: 56 | permalink: true 57 | - pymdownx.betterem 58 | - pymdownx.superfences 59 | - pymdownx.tabbed 60 | - pymdownx.details 61 | - mdx_truly_sane_lists 62 | - pymdownx.superfences: 63 | custom_fences: 64 | - name: mermaid 65 | class: mermaid 66 | format: !!python/name:pymdownx.superfences.fence_code_format 67 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "includeForks": true 6 | } 7 | -------------------------------------------------------------------------------- /rules/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | } 6 | 7 | dependencies { 8 | compileOnlyOrApi(libs.kotlin.compiler) 9 | 10 | testImplementation(libs.junit5) 11 | testImplementation(libs.junit5.params) 12 | testImplementation(libs.assertj) 13 | } 14 | -------------------------------------------------------------------------------- /rules/common/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=common 2 | POM_NAME=Compose rules linter agnostic detectors 3 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/ComposeKtConfig.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core 4 | 5 | interface ComposeKtConfig { 6 | fun getInt(key: String, default: Int): Int 7 | fun getString(key: String, default: String?): String? 8 | fun getList(key: String, default: List): List 9 | fun getSet(key: String, default: Set): Set 10 | fun getBoolean(key: String, default: Boolean): Boolean 11 | } 12 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/ComposeKtVisitor.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core 4 | 5 | import org.jetbrains.kotlin.psi.KtClass 6 | import org.jetbrains.kotlin.psi.KtFile 7 | import org.jetbrains.kotlin.psi.KtFunction 8 | 9 | interface ComposeKtVisitor { 10 | val isOptIn: Boolean 11 | get() = false 12 | 13 | fun visitFunction(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) {} 14 | 15 | fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) {} 16 | 17 | fun visitClass(clazz: KtClass, emitter: Emitter, config: ComposeKtConfig) {} 18 | 19 | fun visitFile(file: KtFile, emitter: Emitter, config: ComposeKtConfig) {} 20 | } 21 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/Emitter.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core 4 | 5 | import org.jetbrains.kotlin.com.intellij.psi.PsiElement 6 | import kotlin.contracts.ExperimentalContracts 7 | import kotlin.contracts.contract 8 | 9 | fun interface Emitter { 10 | fun report(element: PsiElement, errorMessage: String, canBeAutoCorrected: Boolean): Decision 11 | } 12 | 13 | fun Emitter.report(element: PsiElement, errorMessage: String) = 14 | report(element = element, errorMessage = errorMessage, canBeAutoCorrected = false) 15 | 16 | enum class Decision { 17 | Fix, 18 | Ignore, 19 | } 20 | 21 | @OptIn(ExperimentalContracts::class) 22 | fun Decision.ifFix(block: () -> Unit) { 23 | contract { 24 | callsInPlace(block, kotlin.contracts.InvocationKind.AT_MOST_ONCE) 25 | } 26 | if (this == Decision.Fix) block() 27 | } 28 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/ASTNodes.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.com.intellij.lang.ASTNode 6 | import org.jetbrains.kotlin.com.intellij.psi.PsiComment 7 | import org.jetbrains.kotlin.lexer.KtTokens 8 | 9 | fun ASTNode.lastChildLeafOrSelf(): ASTNode { 10 | var node = this 11 | if (node.lastChildNode != null) { 12 | do { 13 | node = node.lastChildNode 14 | } while (node.lastChildNode != null) 15 | return node 16 | } 17 | return node 18 | } 19 | 20 | fun ASTNode.firstChildLeafOrSelf(): ASTNode { 21 | var node = this 22 | if (node.firstChildNode != null) { 23 | do { 24 | node = node.firstChildNode 25 | } while (node.firstChildNode != null) 26 | return node 27 | } 28 | return node 29 | } 30 | 31 | fun ASTNode.parent(p: (ASTNode) -> Boolean, strict: Boolean = true): ASTNode? { 32 | var n: ASTNode? = if (strict) this.treeParent else this 33 | while (n != null) { 34 | if (p(n)) { 35 | return n 36 | } 37 | n = n.treeParent 38 | } 39 | return null 40 | } 41 | 42 | fun ASTNode.isPartOfComment(): Boolean = parent({ it.psi is PsiComment }, strict = false) != null 43 | 44 | fun ASTNode.nextCodeSibling(): ASTNode? = 45 | nextSibling { it.elementType != KtTokens.WHITE_SPACE && !it.isPartOfComment() } 46 | 47 | inline fun ASTNode.nextSibling(p: (ASTNode) -> Boolean): ASTNode? { 48 | var node = treeNext 49 | while (node != null) { 50 | if (p(node)) { 51 | return node 52 | } 53 | node = node.treeNext 54 | } 55 | return null 56 | } 57 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KotlinUtils.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.name.FqName 6 | import org.jetbrains.kotlin.psi.KtElement 7 | import java.util.Locale 8 | 9 | fun T.runIf(value: Boolean, block: T.() -> T): T = if (value) block() else this 10 | 11 | fun T.runIfNotNull(value: R?, block: T.(R) -> T): T = value?.let { block(it) } ?: this 12 | 13 | fun Sequence.mapIf(condition: (T) -> Boolean, transform: (T) -> R): Sequence = 14 | mapNotNull { if (condition(it)) transform(it) else null } 15 | 16 | fun > Sequence.mapFirst() = map { it.first } 17 | fun > Sequence.mapSecond() = map { it.second } 18 | 19 | operator fun FqName.plus(other: String): FqName = FqName(asString() + "." + other) 20 | operator fun FqName.plus(other: FqName): FqName = if (isRoot) other else plus(other.asString()) 21 | 22 | fun String?.matchesAnyOf(patterns: Sequence): Boolean { 23 | if (isNullOrEmpty()) return false 24 | for (regex in patterns) { 25 | if (matches(regex)) return true 26 | } 27 | return false 28 | } 29 | 30 | fun Set.joinToRegexOrNull(): Regex? = if (isEmpty()) null else joinToRegex() 31 | 32 | fun Set.joinToRegex(): Regex = Regex( 33 | joinToString( 34 | separator = "|", 35 | prefix = "(", 36 | postfix = ")", 37 | ), 38 | ) 39 | 40 | fun String.toCamelCase() = split('_').joinToString( 41 | separator = "", 42 | transform = { original -> 43 | original.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } 44 | }, 45 | ) 46 | 47 | fun String.toSnakeCase() = replace(humps, "_").lowercase(Locale.getDefault()) 48 | 49 | private val humps by lazy { "(?<=.)(?=\\p{Upper})".toRegex() } 50 | 51 | val KotlinScopeFunctions = setOf("with", "apply", "run", "also", "let") 52 | val KotlinItObjectScopeFunctions = setOf("let", "also") 53 | 54 | fun Sequence.uniquePairs(): Sequence> = sequence { 55 | val list = toList() 56 | for (i in list.indices) { 57 | for (j in i + 1 until list.size) { 58 | yield(Pair(list[i], list[j])) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtAnnotateds.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.psi.KtAnnotated 6 | import org.jetbrains.kotlin.psi.KtElement 7 | import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry 8 | import org.jetbrains.kotlin.psi.KtStringTemplateExpression 9 | import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf 10 | 11 | val KtAnnotated.isComposable: Boolean 12 | get() = annotationEntries.any { it.calleeExpression?.text == "Composable" } 13 | 14 | fun KtElement.isSuppressed(suppression: String): Boolean = parentsWithSelf.filterIsInstance() 15 | .flatMap { it.annotationEntries } 16 | .filter { it.calleeExpression?.text == "Suppress" } 17 | .flatMap { it.valueArguments } 18 | .map { it.getArgumentExpression() } 19 | .filterIsInstance() 20 | .flatMap { it.entries.asSequence() } 21 | .filterIsInstance() 22 | .any { it.text == suppression } 23 | 24 | fun KtAnnotated.isAnnotatedWith(annotations: Set): Boolean = 25 | annotationEntries.any { it.calleeExpression?.text in annotations } 26 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtCallableDeclarations.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.psi.KtCallableDeclaration 6 | import org.jetbrains.kotlin.psi.KtParameter 7 | 8 | val KtCallableDeclaration.isTypeMutable: Boolean 9 | get() = typeReference?.text?.matchesAnyOf(KnownMutableCommonTypesRegex) == true 10 | 11 | private val KnownMutableCommonTypesRegex = sequenceOf( 12 | // Set 13 | "MutableSet<.*>\\??", 14 | "ArraySet<.*>\\??", 15 | "HashSet<.*>\\??", 16 | // List 17 | "MutableList<.*>\\??", 18 | "ArrayList<.*>\\??", 19 | // Array 20 | "SparseArray<.*>\\??", 21 | "SparseArrayCompat<.*>\\??", 22 | "LongSparseArray<.*>\\??", 23 | "SparseBooleanArray\\??", 24 | "SparseIntArray\\??", 25 | // Map 26 | "MutableMap<.*>\\??", 27 | "HashMap<.*>\\??", 28 | "Hashtable<.*>\\??", 29 | // Flow 30 | "MutableStateFlow<.*>\\??", 31 | "MutableSharedFlow<.*>\\??", 32 | // RxJava & RxRelay 33 | "PublishSubject<.*>\\??", 34 | "BehaviorSubject<.*>\\??", 35 | "ReplaySubject<.*>\\??", 36 | "PublishRelay<.*>\\??", 37 | "BehaviorRelay<.*>\\??", 38 | "ReplayRelay<.*>\\??", 39 | ).map { Regex(it) } 40 | 41 | val KtCallableDeclaration.isTypeUnstableCollection: Boolean 42 | get() = typeReference?.text?.matchesAnyOf(KnownUnstableCollectionTypesRegex) == true 43 | 44 | val KnownUnstableCollectionTypesRegex = sequenceOf( 45 | "Set<.*>\\??", 46 | "List<.*>\\??", 47 | "Map<.*>\\??", 48 | ).map { Regex(it) } 49 | 50 | fun KtCallableDeclaration.contentSlots( 51 | treatAsLambdaTypes: Set, 52 | treatAsComposableLambdaTypes: Set, 53 | ): Sequence = valueParameters.asSequence() 54 | .filter { parameter -> 55 | parameter.typeReference?.isComposableUiEmitterLambda(treatAsLambdaTypes, treatAsComposableLambdaTypes) == true 56 | } 57 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtConstantExpressions.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.KtNodeTypes 6 | import org.jetbrains.kotlin.psi.KtConstantExpression 7 | 8 | val KtConstantExpression.isInt: Boolean 9 | get() = isIntegerConstant && !isLong 10 | 11 | val KtConstantExpression.isLong: Boolean 12 | get() = isIntegerConstant && text.endsWith("L") 13 | 14 | val KtConstantExpression.isDouble: Boolean 15 | get() = isFloatConstant && !isFloat 16 | 17 | val KtConstantExpression.isFloat: Boolean 18 | get() = isFloatConstant && text.endsWith("f") 19 | 20 | private val KtConstantExpression.isIntegerConstant: Boolean 21 | get() = node.elementType == KtNodeTypes.INTEGER_CONSTANT 22 | 23 | private val KtConstantExpression.isFloatConstant: Boolean 24 | get() = node.elementType == KtNodeTypes.FLOAT_CONSTANT 25 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtDotQualifiedExpressions.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.psi.KtDotQualifiedExpression 6 | import org.jetbrains.kotlin.psi.KtExpression 7 | import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf 8 | 9 | val KtDotQualifiedExpression.rootExpression: KtExpression 10 | get() { 11 | var current: KtExpression = receiverExpression 12 | while (current is KtDotQualifiedExpression) { 13 | current = current.receiverExpression 14 | } 15 | return current 16 | } 17 | 18 | /** 19 | * [KtDotQualifiedExpression] can be nested, and if we only care about the one that contains all the expression, 20 | * this method will filter out all the others from the [Sequence]. 21 | * 22 | * For example: "androidx.compose.material" would have "androidx.compose.material", "androidx.compose" and "androidx". 23 | * If these were in the same sequence, we'd only use "androidx.compose.material" and get rid of the others. 24 | */ 25 | fun Sequence.dedupUsingOutermost(): Sequence = 26 | map { it.outermost }.distinct() 27 | 28 | val KtDotQualifiedExpression.outermost: KtDotQualifiedExpression 29 | get() = parentsWithSelf.takeWhile { it is KtDotQualifiedExpression }.last() as KtDotQualifiedExpression 30 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtFunctions.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.lexer.KtTokens 6 | import org.jetbrains.kotlin.psi.KtClass 7 | import org.jetbrains.kotlin.psi.KtClassBody 8 | import org.jetbrains.kotlin.psi.KtFile 9 | import org.jetbrains.kotlin.psi.KtFunction 10 | import org.jetbrains.kotlin.psi.KtModifierListOwner 11 | import org.jetbrains.kotlin.psi.KtNamedFunction 12 | import org.jetbrains.kotlin.psi.psiUtil.parents 13 | import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType 14 | 15 | val KtFunction.returnsValue: Boolean 16 | get() = typeReference != null && typeReference!!.text != "Unit" 17 | 18 | val KtFunction.hasReceiverType: Boolean 19 | get() = receiverTypeReference != null 20 | 21 | val KtModifierListOwner.isPrivate: Boolean 22 | get() = visibilityModifierType() == KtTokens.PRIVATE_KEYWORD 23 | 24 | val KtModifierListOwner.isProtected: Boolean 25 | get() = visibilityModifierType() == KtTokens.PROTECTED_KEYWORD 26 | 27 | val KtModifierListOwner.isInternal: Boolean 28 | get() = visibilityModifierType() == KtTokens.INTERNAL_KEYWORD 29 | 30 | val KtFunction.isOverride: Boolean 31 | get() = hasModifier(KtTokens.OVERRIDE_KEYWORD) 32 | 33 | val KtFunction.isActual: Boolean 34 | get() = hasModifier(KtTokens.ACTUAL_KEYWORD) 35 | 36 | val KtFunction.isExpect: Boolean 37 | get() = hasModifier(KtTokens.EXPECT_KEYWORD) 38 | 39 | val KtFunction.isAbstract: Boolean 40 | get() = hasModifier(KtTokens.ABSTRACT_KEYWORD) 41 | 42 | val KtFunction.isOpen: Boolean 43 | get() = hasModifier(KtTokens.OPEN_KEYWORD) 44 | 45 | val KtFunction.isOperator: Boolean 46 | get() = hasModifier(KtTokens.OPERATOR_KEYWORD) 47 | 48 | val KtFunction.definedInInterface: Boolean 49 | get() = ((parent as? KtClassBody)?.parent as? KtClass)?.isInterface() ?: false 50 | 51 | val KtNamedFunction.isNested: Boolean 52 | get() = parents.takeWhile { it !is KtFile }.any { it is KtNamedFunction } 53 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtImportLists.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.KtNodeTypes 6 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement 7 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.ElementType 8 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement 9 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl 10 | import org.jetbrains.kotlin.psi.KtImportList 11 | import org.jetbrains.kotlin.psi.psiUtil.children 12 | 13 | fun KtImportList.sort() { 14 | val sortedImports = node.children() 15 | .filter { it.elementType == KtNodeTypes.IMPORT_DIRECTIVE } 16 | .sortedBy { it.text } 17 | .distinctBy { it.text } 18 | node.removeRange(node.firstChildNode, node.lastChildNode.treeNext) 19 | sortedImports.forEachIndexed { index, astNode -> 20 | if (index > 0) { 21 | node.addChild(PsiWhiteSpaceImpl("\n"), null) 22 | } 23 | node.addChild(astNode, null) 24 | } 25 | } 26 | 27 | fun KtImportList.addImports(vararg imports: String) { 28 | imports.forEach { import -> 29 | val newImport = CompositeElement(KtNodeTypes.IMPORT_DIRECTIVE).apply { 30 | rawAddChildren(LeafPsiElement(ElementType.IMPORT_KEYWORD, "import")) 31 | rawAddChildren(LeafPsiElement(ElementType.WHITE_SPACE, " ")) 32 | import.split('.').forEachIndexed { index, s -> 33 | if (index != 0) { 34 | rawAddChildren(LeafPsiElement(ElementType.DOT, ".")) 35 | } 36 | rawAddChildren(LeafPsiElement(ElementType.IDENTIFIER, s)) 37 | } 38 | } 39 | node.addChild(newImport, null) 40 | } 41 | sort() 42 | } 43 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/KtParameters.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.psi.KtNullableType 6 | import org.jetbrains.kotlin.psi.KtParameter 7 | 8 | val KtParameter.isTypeNullable: Boolean 9 | get() = typeReference?.typeElement is KtNullableType 10 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/Previews.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.psi.KtAnnotated 6 | import org.jetbrains.kotlin.psi.KtAnnotationEntry 7 | 8 | val KtAnnotated.isPreview: Boolean 9 | get() = annotationEntries.any { it.isPreviewAnnotation } 10 | 11 | val KtAnnotationEntry.isPreviewAnnotation: Boolean 12 | get() = calleeExpression?.text?.contains("Preview") == true 13 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/core/util/PsiElements.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.core.util 4 | 5 | import org.jetbrains.kotlin.com.intellij.psi.PsiElement 6 | import org.jetbrains.kotlin.com.intellij.psi.PsiNameIdentifierOwner 7 | import org.jetbrains.kotlin.psi.psiUtil.endOffset 8 | import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf 9 | import org.jetbrains.kotlin.psi.psiUtil.siblings 10 | import org.jetbrains.kotlin.psi.psiUtil.startOffset 11 | import java.util.Deque 12 | import java.util.LinkedList 13 | 14 | val PsiElementAlwaysTruePredicate: (PsiElement) -> Boolean = { true } 15 | 16 | inline fun PsiElement.findChildrenByClass( 17 | noinline shouldVisitChildren: (PsiElement) -> Boolean = PsiElementAlwaysTruePredicate, 18 | ): Sequence = sequence { 19 | val queue: Deque = LinkedList() 20 | queue.add(this@findChildrenByClass) 21 | while (queue.isNotEmpty()) { 22 | val current = queue.pop() 23 | if (current is T) { 24 | yield(current) 25 | } 26 | if (shouldVisitChildren(current)) { 27 | queue.addAll(current.children) 28 | } 29 | } 30 | } 31 | 32 | inline fun PsiElement.findDirectFirstChildByClass(): T? { 33 | var current = firstChild 34 | while (current != null) { 35 | if (current is T) { 36 | return current 37 | } 38 | current = current.nextSibling 39 | } 40 | return null 41 | } 42 | 43 | inline fun PsiElement.findDirectChildrenByClass(): Sequence = sequence { 44 | var current = firstChild 45 | while (current != null) { 46 | if (current is T) { 47 | yield(current) 48 | } 49 | current = current.nextSibling 50 | } 51 | } 52 | 53 | fun PsiElement.walkBackwards(stopAtParent: PsiElement? = null): Sequence = parentsWithSelf 54 | .flatMap { it.siblings(forward = false, withItself = true) } 55 | .takeWhile { it != stopAtParent } 56 | 57 | val PsiNameIdentifierOwner.startOffsetFromName: Int 58 | get() = nameIdentifier?.startOffset ?: startOffset 59 | 60 | val PsiElement.range: IntRange 61 | get() = IntRange(startOffset, endOffset) 62 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/ComposableAnnotationNaming.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import org.jetbrains.kotlin.psi.KtAnnotated 10 | import org.jetbrains.kotlin.psi.KtClass 11 | 12 | class ComposableAnnotationNaming : ComposeKtVisitor { 13 | override fun visitClass(clazz: KtClass, emitter: Emitter, config: ComposeKtConfig) { 14 | if (!clazz.isAnnotation()) return 15 | if (!clazz.isComposableTargetMarkerAnnotation) return 16 | 17 | val name = clazz.nameAsSafeName.asString() 18 | if (!name.endsWith("Composable")) { 19 | emitter.report(clazz, ComposableAnnotationDoesNotEndWithComposable) 20 | } 21 | } 22 | 23 | private val KtAnnotated.isComposableTargetMarkerAnnotation: Boolean 24 | get() = annotationEntries.any { 25 | it.calleeExpression?.text?.contains("ComposableTargetMarker") == true 26 | } 27 | 28 | companion object { 29 | val ComposableAnnotationDoesNotEndWithComposable = """ 30 | Composable annotations (e.g. tagged with `@ComposableTargetMarker`) should have the `Composable` suffix. 31 | See https://mrmans0n.github.io/compose-rules/rules/#naming-composable-annotations-properly for more information. 32 | """.trimIndent() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/CompositionLocalAllowlist.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.declaresCompositionLocal 10 | import io.nlopez.compose.core.util.findChildrenByClass 11 | import org.jetbrains.kotlin.psi.KtFile 12 | import org.jetbrains.kotlin.psi.KtProperty 13 | 14 | class CompositionLocalAllowlist : ComposeKtVisitor { 15 | 16 | override fun visitFile(file: KtFile, emitter: Emitter, config: ComposeKtConfig) { 17 | val compositionLocals = file.findChildrenByClass() 18 | .filter { it.declaresCompositionLocal } 19 | 20 | if (compositionLocals.none()) return 21 | 22 | val allowed = config.getSet("allowedCompositionLocals", emptySet()) 23 | val notAllowed = compositionLocals.filterNot { allowed.contains(it.nameIdentifier?.text) } 24 | 25 | for (compositionLocal in notAllowed) { 26 | emitter.report( 27 | compositionLocal, 28 | CompositionLocalNotInAllowlist, 29 | ) 30 | } 31 | } 32 | 33 | companion object { 34 | val CompositionLocalNotInAllowlist = """ 35 | CompositionLocals are implicit dependencies and creating new ones should be avoided. 36 | See https://mrmans0n.github.io/compose-rules/rules/#compositionlocals for more information. 37 | """.trimIndent() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/CompositionLocalNaming.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.declaresCompositionLocal 10 | import io.nlopez.compose.core.util.findChildrenByClass 11 | import org.jetbrains.kotlin.psi.KtFile 12 | import org.jetbrains.kotlin.psi.KtProperty 13 | 14 | class CompositionLocalNaming : ComposeKtVisitor { 15 | 16 | override fun visitFile(file: KtFile, emitter: Emitter, config: ComposeKtConfig) { 17 | val compositionLocals = file.findChildrenByClass() 18 | .filter { it.declaresCompositionLocal } 19 | 20 | if (compositionLocals.none()) return 21 | 22 | val notAllowed = compositionLocals.filterNot { it.nameIdentifier?.text?.startsWith("Local") == true } 23 | 24 | for (compositionLocal in notAllowed) { 25 | emitter.report(compositionLocal, CompositionLocalNeedsLocalPrefix) 26 | } 27 | } 28 | 29 | companion object { 30 | val CompositionLocalNeedsLocalPrefix = """ 31 | CompositionLocals should be named using the `Local` prefix as an adjective, followed by a descriptive noun. 32 | See https://mrmans0n.github.io/compose-rules/rules/#naming-compositionlocals-properly for more information. 33 | """.trimIndent() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/ContentSlotReused.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.composableLambdaTypes 10 | import io.nlopez.compose.core.util.contentSlots 11 | import io.nlopez.compose.core.util.findChildrenByClass 12 | import io.nlopez.compose.core.util.findShadowingRedeclarations 13 | import io.nlopez.compose.core.util.isTypeNullable 14 | import io.nlopez.compose.core.util.lambdaTypes 15 | import org.jetbrains.kotlin.psi.KtCallExpression 16 | import org.jetbrains.kotlin.psi.KtFunction 17 | import org.jetbrains.kotlin.psi.KtParameter 18 | import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression 19 | 20 | class ContentSlotReused : ComposeKtVisitor { 21 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 22 | val lambdaTypes = function.containingKtFile.lambdaTypes(config) 23 | val composableLambdaTypes = function.containingKtFile.composableLambdaTypes(config) 24 | 25 | val slotsWithMultipleUsages = function.contentSlots(lambdaTypes, composableLambdaTypes) 26 | .filter { slot -> function.findNotShadowedUsagesOf(slot).count() >= 2 } 27 | 28 | for (slot in slotsWithMultipleUsages) { 29 | emitter.report(slot, ContentSlotsShouldNotBeReused) 30 | } 31 | } 32 | 33 | private fun KtFunction.findNotShadowedUsagesOf(slot: KtParameter): Sequence { 34 | val slotName = slot.name?.takeIf { it.isNotEmpty() } ?: return emptySequence() 35 | val slots = when { 36 | // content?.invoke() 37 | slot.isTypeNullable -> findChildrenByClass() 38 | .filter { it.receiverExpression.text == slotName } 39 | .mapNotNull { it.selectorExpression as? KtCallExpression } 40 | .filter { it.calleeExpression?.text == "invoke" } 41 | 42 | // content() 43 | else -> findChildrenByClass().filter { it.calleeExpression?.text == slotName } 44 | } 45 | // Return and remove shadowed usages 46 | return slots.filter { it.findShadowingRedeclarations(parameterName = slotName, stopAt = this).count() == 0 } 47 | } 48 | 49 | companion object { 50 | val ContentSlotsShouldNotBeReused = """ 51 | Content slots should not be reused in different code branches/scopes of a composable function, to preserve the slot internal state. 52 | You can wrap the slot in a remember { movableContentOf { ... }} block to make sure their internal state is preserved correctly. 53 | See https://mrmans0n.github.io/compose-rules/rules/#content-slots-should-not-be-reused-in-branching-code for more information. 54 | """.trimIndent() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/ContentTrailingLambda.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.composableLambdaTypes 10 | import io.nlopez.compose.core.util.isComposableLambda 11 | import io.nlopez.compose.core.util.lambdaTypes 12 | import org.jetbrains.kotlin.psi.KtFunction 13 | 14 | class ContentTrailingLambda : ComposeKtVisitor { 15 | 16 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 17 | val lambdaTypes = function.containingKtFile.lambdaTypes(config) 18 | val composableLambdaTypes = function.containingKtFile.composableLambdaTypes(config) 19 | 20 | val candidate = function.valueParameters 21 | .filter { it.name == "content" } 22 | .singleOrNull { parameter -> 23 | parameter.typeReference?.isComposableLambda(lambdaTypes, composableLambdaTypes) == true 24 | } 25 | 26 | if (candidate != null && candidate != function.valueParameters.last()) { 27 | emitter.report(candidate, ContentShouldBeTrailingLambda) 28 | } 29 | } 30 | 31 | companion object { 32 | val ContentShouldBeTrailingLambda = """ 33 | A @Composable `content` parameter should be moved to be the trailing lambda in a composable function. 34 | See https://mrmans0n.github.io/compose-rules/rules/#slots-for-main-content-should-be-the-trailing-lambda for more information. 35 | """.trimIndent() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/LambdaParameterEventTrailing.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.emitsContent 10 | import io.nlopez.compose.core.util.isComposable 11 | import io.nlopez.compose.core.util.isLambda 12 | import io.nlopez.compose.core.util.modifierParameter 13 | import org.jetbrains.kotlin.psi.KtFunction 14 | 15 | class LambdaParameterEventTrailing : ComposeKtVisitor { 16 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 17 | // We want this rule to only run for functions that emit content 18 | if (!function.emitsContent(config)) return 19 | 20 | // We'd also want for it to have a modifier to apply the rule, which serves two purposes: 21 | // - making sure that it's the separation between required and optional parameters 22 | // - the lambda would be able to be moved before the modifier and not be the trailing one 23 | if (function.modifierParameter(config) == null) return 24 | 25 | val trailingParam = function.valueParameters.lastOrNull() ?: return 26 | 27 | // Check if the trailing element... 28 | // - is a lambda 29 | // - is not @Composable 30 | // - doesn't have a default value 31 | // - starts with "on", meaning it's an event 32 | val typeReference = trailingParam.typeReference ?: return 33 | if (!typeReference.isLambda()) return 34 | if (typeReference.isComposable) return 35 | if (trailingParam.hasDefaultValue()) return 36 | val name = trailingParam.name ?: return 37 | if (!name.startsWith("on")) return 38 | 39 | emitter.report(trailingParam, EventLambdaIsTrailingLambda) 40 | } 41 | 42 | companion object { 43 | val EventLambdaIsTrailingLambda = """ 44 | Lambda parameters in a @Composable that are for events (e.g. onClick, onChange, etc) and are required (they don't have a default value) should not be used as the trailing parameter. 45 | Composable functions that emit content usually reserve the trailing lambda syntax for the content slot, and that can lead to an assumption that other composables can be used in that lambda. 46 | See https://mrmans0n.github.io/compose-rules/rules/#avoid-using-the-trailing-lambda-for-event-lambdas-in-ui-composables for more information. 47 | """.trimIndent() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/ModifierComposed.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.findDirectChildrenByClass 10 | import io.nlopez.compose.core.util.isComposable 11 | import io.nlopez.compose.core.util.isModifierReceiver 12 | import org.jetbrains.kotlin.psi.KtCallExpression 13 | import org.jetbrains.kotlin.psi.KtFunction 14 | import org.jetbrains.kotlin.psi.KtReturnExpression 15 | 16 | class ModifierComposed : ComposeKtVisitor { 17 | 18 | override fun visitFunction(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 19 | if (!function.isModifierReceiver(config)) return 20 | if (function.isComposable) return 21 | 22 | // If using a body expression, we can directly check for it being a call to `composed` 23 | val bodyExpression = function.bodyExpression 24 | if (bodyExpression is KtCallExpression && bodyExpression.calleeExpression?.text == "composed") { 25 | emitter.report(function, ComposedModifier) 26 | } 27 | 28 | // Otherwise, check the return statement expression 29 | val bodyBlockExpression = function.bodyBlockExpression ?: return 30 | val returnsComposed = bodyBlockExpression.findDirectChildrenByClass() 31 | .mapNotNull { it.returnedExpression } 32 | .filterIsInstance() 33 | .any { it.calleeExpression?.text == "composed" } 34 | 35 | if (returnsComposed) { 36 | emitter.report(function, ComposedModifier) 37 | } 38 | } 39 | 40 | companion object { 41 | val ComposedModifier = """ 42 | Using composed for modifiers is not recommended anymore, due to the performance issues it creates. 43 | You should consider migrating this modifier to be based on Modifier.Node instead. 44 | See https://mrmans0n.github.io/compose-rules/rules/#avoid-modifier-extension-factory-functions for more information. 45 | """.trimIndent() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/ModifierNaming.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.isModifier 10 | import org.jetbrains.kotlin.psi.KtFunction 11 | 12 | class ModifierNaming : ComposeKtVisitor { 13 | 14 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 15 | // If there is a modifier param, we bail 16 | val modifiers = function.valueParameters.filter { it.isModifier(config) } 17 | 18 | // If there are no modifiers, or more than one, we don't care as much about the naming 19 | if (modifiers.isEmpty()) return 20 | 21 | val count = modifiers.size 22 | if (count == 1) { 23 | if (modifiers.first().name?.equals("modifier") != true) { 24 | emitter.report(modifiers.first(), ModifiersAreSupposedToBeCalledModifierWhenAlone) 25 | return 26 | } 27 | } else { 28 | for (modifier in modifiers) { 29 | val valid = modifier.name?.lowercase()?.endsWith("modifier") ?: false 30 | if (!valid) { 31 | emitter.report(modifier, ModifiersAreSupposedToEndInModifierWhenMultiple) 32 | } 33 | } 34 | } 35 | } 36 | 37 | companion object { 38 | val ModifiersAreSupposedToBeCalledModifierWhenAlone = """ 39 | Modifier parameters should be called `modifier`. 40 | See https://mrmans0n.github.io/compose-rules/rules/#naming-modifiers-properly for more information. 41 | """.trimIndent() 42 | val ModifiersAreSupposedToEndInModifierWhenMultiple = """ 43 | Modifier parameters should be called `modifier` or end in `Modifier` if there are more than one in the same @Composable. 44 | See https://mrmans0n.github.io/compose-rules/rules/#naming-modifiers-properly for more information. 45 | """.trimIndent() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/ModifierWithoutDefault.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.ifFix 9 | import io.nlopez.compose.core.util.definedInInterface 10 | import io.nlopez.compose.core.util.isAbstract 11 | import io.nlopez.compose.core.util.isActual 12 | import io.nlopez.compose.core.util.isModifier 13 | import io.nlopez.compose.core.util.isModifierReceiver 14 | import io.nlopez.compose.core.util.isOpen 15 | import io.nlopez.compose.core.util.isOverride 16 | import io.nlopez.compose.core.util.lastChildLeafOrSelf 17 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement 18 | import org.jetbrains.kotlin.psi.KtFunction 19 | 20 | class ModifierWithoutDefault : ComposeKtVisitor { 21 | 22 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 23 | if ( 24 | function.definedInInterface || 25 | function.isActual || 26 | function.isOverride || 27 | function.isAbstract || 28 | function.isOpen || 29 | function.isModifierReceiver(config) 30 | ) { 31 | return 32 | } 33 | 34 | // Look for modifier params in the composable signature, and if any without a default value is found, error out. 35 | function.valueParameters.filter { it.isModifier(config) } 36 | .filterNot { it.hasDefaultValue() } 37 | .forEach { modifierParameter -> 38 | emitter.report(modifierParameter, MissingModifierDefaultParam, true).ifFix { 39 | // This error is easily auto fixable, we just inject ` = Modifier` to the param 40 | val lastToken = modifierParameter.node.lastChildLeafOrSelf() as LeafPsiElement 41 | val currentText = lastToken.text 42 | lastToken.rawReplaceWithText("$currentText = Modifier") 43 | } 44 | } 45 | } 46 | 47 | companion object { 48 | val MissingModifierDefaultParam = """ 49 | This @Composable function has a modifier parameter but it doesn't have a default value. 50 | See https://mrmans0n.github.io/compose-rules/rules/#modifiers-should-have-default-parameters for more information. 51 | """.trimIndent() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/MutableParameters.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.isTypeMutable 10 | import org.jetbrains.kotlin.psi.KtFunction 11 | 12 | class MutableParameters : ComposeKtVisitor { 13 | 14 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 15 | function.valueParameters 16 | .filter { it.isTypeMutable } 17 | .forEach { emitter.report(it, MutableParameterInCompose) } 18 | } 19 | 20 | companion object { 21 | 22 | val MutableParameterInCompose = """ 23 | Using mutable objects as state in Compose will cause your users to see incorrect or stale data in your app. 24 | Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change. 25 | See https://mrmans0n.github.io/compose-rules/rules/#do-not-use-inherently-mutable-types-as-parameters for more information. 26 | """.trimIndent() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/MutableStateParameter.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import org.jetbrains.kotlin.psi.KtFunction 10 | 11 | class MutableStateParameter : ComposeKtVisitor { 12 | 13 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 14 | function.valueParameters 15 | .filter { it.typeReference?.text?.matches(MutableStateRegex) == true } 16 | .forEach { emitter.report(it, MutableStateParameterInCompose) } 17 | } 18 | 19 | companion object { 20 | private val MutableStateRegex = "MutableState<.*>\\??".toRegex() 21 | 22 | val MutableStateParameterInCompose = """ 23 | MutableState shouldn't be used as a parameter in a @Composable function, as it promotes joint ownership over a state between a component and its user. 24 | If possible, consider making the component stateless and concede the state change to the caller. If mutation of the parent’s owned property is required in the component, consider creating a ComponentState class with the domain specific meaningful field that is backed by mutableStateOf(). 25 | See https://mrmans0n.github.io/compose-rules/rules/#do-not-use-mutablestate-as-a-parameter for more information. 26 | """.trimIndent() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/PreviewAnnotationNaming.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.isPreview 10 | import org.jetbrains.kotlin.psi.KtClass 11 | 12 | class PreviewAnnotationNaming : ComposeKtVisitor { 13 | override fun visitClass(clazz: KtClass, emitter: Emitter, config: ComposeKtConfig) { 14 | if (!clazz.isAnnotation()) return 15 | if (!clazz.isPreview) return 16 | 17 | val name = clazz.nameAsSafeName.asString() 18 | if (!name.startsWith("Preview")) { 19 | emitter.report(clazz, PreviewAnnotationDoesNotStartWithPreview) 20 | } 21 | } 22 | 23 | companion object { 24 | val PreviewAnnotationDoesNotStartWithPreview = """ 25 | MultiPreview annotations should start with `Preview` as prefix. 26 | See https://mrmans0n.github.io/compose-rules/rules/#naming-multipreview-annotations-properly for more information. 27 | """.trimIndent() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/PreviewPublic.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.ifFix 9 | import io.nlopez.compose.core.util.firstChildLeafOrSelf 10 | import io.nlopez.compose.core.util.isPreview 11 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement 12 | import org.jetbrains.kotlin.lexer.KtTokens 13 | import org.jetbrains.kotlin.psi.KtFunction 14 | import org.jetbrains.kotlin.psi.psiUtil.isPublic 15 | 16 | class PreviewPublic : ComposeKtVisitor { 17 | 18 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 19 | // We only want previews 20 | if (!function.isPreview) return 21 | // We only care about public methods 22 | if (!function.isPublic) return 23 | 24 | emitter.report(function, ComposablesPreviewShouldNotBePublic, true).ifFix { 25 | // Ideally if the kotlin embeddable compiler exposes what we need, this would be it: 26 | // function.addModifier(KtTokens.PRIVATE_KEYWORD) 27 | 28 | // For now we need to do it by hand with ASTNode: find the "fun" modifier, and prepend "private". 29 | val node = function.node.findChildByType(KtTokens.FUN_KEYWORD) 30 | ?.firstChildLeafOrSelf() as? LeafPsiElement 31 | ?: return@ifFix 32 | node.rawReplaceWithText(KtTokens.PRIVATE_KEYWORD.value + " " + KtTokens.FUN_KEYWORD.value) 33 | } 34 | } 35 | 36 | companion object { 37 | val ComposablesPreviewShouldNotBePublic = """ 38 | Composables annotated with @Preview that are used only for previewing the UI should not be public. 39 | See https://mrmans0n.github.io/compose-rules/rules/#preview-composables-should-not-be-public for more information. 40 | """.trimIndent() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/RememberContentMissing.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.util.findChildrenByClass 9 | import io.nlopez.compose.core.util.isRemembered 10 | import org.jetbrains.kotlin.psi.KtCallExpression 11 | import org.jetbrains.kotlin.psi.KtFunction 12 | 13 | class RememberContentMissing : ComposeKtVisitor { 14 | 15 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 16 | // To keep memory consumption in check, we first traverse down until we see one of our known functions 17 | // that need remembering 18 | function.findChildrenByClass() 19 | .filter { it.calleeExpression?.text in ContentThatNeedsRemembering } 20 | // Only for those, we traverse up to [function], to see if it was actually remembered 21 | .filterNot { it.isRemembered(function) } 22 | // If it wasn't, we show the error 23 | .forEach { callExpression -> 24 | when (callExpression.calleeExpression!!.text) { 25 | "movableContentOf" -> emitter.report( 26 | element = callExpression, 27 | errorMessage = MovableContentOfNotRemembered, 28 | canBeAutoCorrected = false, 29 | ) 30 | 31 | "movableContentWithReceiverOf" -> emitter.report( 32 | element = callExpression, 33 | errorMessage = MovableContentWithReceiverOfNotRemembered, 34 | canBeAutoCorrected = false, 35 | ) 36 | } 37 | } 38 | } 39 | 40 | companion object { 41 | private val ContentThatNeedsRemembering = setOf( 42 | "movableContentOf", 43 | "movableContentWithReceiverOf", 44 | ) 45 | 46 | val MovableContentOfNotRemembered = errorMessage("movableContentOf") 47 | val MovableContentWithReceiverOfNotRemembered = errorMessage("movableContentWithReceiverOf") 48 | private fun errorMessage(name: String): String = """ 49 | Using `$name` in a @Composable function without it being remembered can cause visual problems, as the content would be recycled when detached from the composition. 50 | See https://mrmans0n.github.io/compose-rules/rules/#movable-content-should-be-remembered for more information. 51 | """.trimIndent() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/RememberStateMissing.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.util.findChildrenByClass 9 | import io.nlopez.compose.core.util.isRemembered 10 | import org.jetbrains.kotlin.psi.KtCallExpression 11 | import org.jetbrains.kotlin.psi.KtFunction 12 | 13 | class RememberStateMissing : ComposeKtVisitor { 14 | 15 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 16 | // To keep memory consumption in check, we first traverse down until we see one of our known functions 17 | // that need remembering 18 | function.findChildrenByClass() 19 | .filter { it.calleeExpression?.text in MethodsThatNeedRemembering } 20 | // Only for those, we traverse up to [function], to see if it was actually remembered 21 | .filterNot { it.isRemembered(function) } 22 | // If it wasn't, we show the error 23 | .forEach { callExpression -> 24 | val errorMessage = MethodsAndErrorsThatNeedRemembering[callExpression.calleeExpression!!.text].orEmpty() 25 | emitter.report(callExpression, errorMessage, false) 26 | } 27 | } 28 | 29 | companion object { 30 | private val MethodsThatNeedRemembering = setOf( 31 | "derivedStateOf", 32 | "mutableStateOf", 33 | "mutableIntStateOf", 34 | "mutableFloatStateOf", 35 | "mutableDoubleStateOf", 36 | "mutableLongStateOf", 37 | ) 38 | private val MethodsAndErrorsThatNeedRemembering = MethodsThatNeedRemembering.associateWith { errorMessage(it) } 39 | 40 | fun errorMessage(name: String): String = """ 41 | Using `$name` in a @Composable function without it being inside of a remember function. 42 | If you don't remember the state instance, a new state instance will be created when the function is recomposed. 43 | See https://mrmans0n.github.io/compose-rules/rules/#state-should-be-remembered-in-composables for more information. 44 | """.trimIndent() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rules/common/src/main/kotlin/io/nlopez/compose/rules/UnstableCollections.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.core.report 9 | import io.nlopez.compose.core.util.isTypeUnstableCollection 10 | import org.jetbrains.kotlin.psi.KtFunction 11 | import java.util.* 12 | 13 | class UnstableCollections : ComposeKtVisitor { 14 | override val isOptIn: Boolean = true 15 | 16 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 17 | for (param in function.valueParameters.filter { it.isTypeUnstableCollection }) { 18 | val variableName = param.nameAsSafeName.asString() 19 | val type = param.typeReference?.text ?: "List/Set/Map" 20 | val message = createErrorMessage( 21 | type = type, 22 | rawType = type.replace(DiamondRegex, ""), 23 | variable = variableName, 24 | ) 25 | emitter.report(param.typeReference ?: param, message) 26 | } 27 | } 28 | 29 | companion object { 30 | private val DiamondRegex by lazy { Regex("<.*>\\??") } 31 | private val String.capitalized: String 32 | get() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } 33 | 34 | fun createErrorMessage(type: String, rawType: String, variable: String) = """ 35 | The Compose Compiler cannot infer the stability of a parameter if a $type is used in it, even if the item type is stable. 36 | You should use Kotlinx Immutable Collections instead: `$variable: Immutable$type` or create an `@Immutable` wrapper for this class: `@Immutable data class ${variable.capitalized}$rawType(val items: $type)` 37 | See https://mrmans0n.github.io/compose-rules/rules/#avoid-using-unstable-collections for more information. 38 | """.trimIndent() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rules/detekt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.kotlin.serialization) 6 | alias(libs.plugins.shadowJar) 7 | } 8 | 9 | // if publishing and it's not the uber jar, we want to remove the shadowRuntimeElements variant 10 | if (!project.hasProperty("uberJar")) { 11 | val javaComponent = components["java"] as AdhocComponentWithVariants 12 | javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { 13 | skip() 14 | } 15 | } 16 | 17 | tasks.shadowJar { 18 | // Relocate packages that may conflict with the ones IntelliJ IDEA provides as well. 19 | // See https://github.com/nbadal/ktlint-intellij-plugin/blob/main/lib/build.gradle.kts 20 | relocate("org.jetbrains.concurrency", "shadow.org.jetbrains.concurrency") 21 | relocate("org.jetbrains.kotlin.psi.KtPsiFactory", "shadow.org.jetbrains.kotlin.psi.KtPsiFactory") 22 | relocate("org.jetbrains.kotlin.psi.psiUtil", "shadow.org.jetbrains.kotlin.psi.psiUtil") 23 | relocate("org.jetbrains.org", "shadow.org.jetbrains.org") 24 | } 25 | 26 | dependencies { 27 | api(libs.detekt.core) 28 | compileOnlyOrApi(projects.rules.common) 29 | 30 | testImplementation(libs.detekt.test) 31 | testImplementation(libs.junit5) 32 | testImplementation(libs.junit5.params) 33 | testImplementation(libs.assertj) 34 | testImplementation(libs.reflections) 35 | testImplementation(libs.kaml) 36 | testImplementation(libs.konsist) 37 | testImplementation(libs.kotlin.compiler) 38 | } 39 | -------------------------------------------------------------------------------- /rules/detekt/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=detekt 2 | POM_NAME=Compose rules for Detekt 3 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/DetektComposeKtConfig.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.internal.valueOrDefaultCommaSeparated 7 | import io.nlopez.compose.core.ComposeKtConfig 8 | 9 | /** 10 | * Manages the configuration for detekt rules. Results will be memoized, as config shouldn't be changing 11 | * during the lifetime of a rule. 12 | */ 13 | internal class DetektComposeKtConfig(private val config: Config) : ComposeKtConfig { 14 | private val cache = mutableMapOf() 15 | 16 | @Suppress("UNCHECKED_CAST") 17 | private fun valueOrPut(key: String, value: () -> T?): T? = cache.getOrPut(key) { value() } as? T 18 | 19 | override fun getInt(key: String, default: Int): Int = 20 | valueOrPut(key) { config.valueOrDefault(key, default) } ?: default 21 | 22 | override fun getString(key: String, default: String?): String? = valueOrPut(key) { 23 | if (default == null) { 24 | config.valueOrNull(key) 25 | } else { 26 | config.valueOrDefault(key, default) 27 | } 28 | } 29 | 30 | override fun getList(key: String, default: List): List = 31 | valueOrPut(key) { config.valueOrDefaultCommaSeparated(key, default) } ?: default 32 | 33 | override fun getSet(key: String, default: Set): Set = 34 | valueOrPut(key) { getList(key, default.toList()).toSet() } ?: default 35 | 36 | override fun getBoolean(key: String, default: Boolean): Boolean = 37 | valueOrPut(key) { config.valueOrDefault(key, default) } ?: default 38 | } 39 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/DetektRule.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.gitlab.arturbosch.detekt.api.CodeSmell 6 | import io.gitlab.arturbosch.detekt.api.Config 7 | import io.gitlab.arturbosch.detekt.api.CorrectableCodeSmell 8 | import io.gitlab.arturbosch.detekt.api.Entity 9 | import io.gitlab.arturbosch.detekt.api.Location 10 | import io.gitlab.arturbosch.detekt.api.Rule 11 | import io.nlopez.compose.core.ComposeKtVisitor 12 | import io.nlopez.compose.core.Decision 13 | import io.nlopez.compose.core.Emitter 14 | import io.nlopez.compose.core.util.isComposable 15 | import io.nlopez.compose.core.util.runIf 16 | import org.jetbrains.kotlin.com.intellij.psi.PsiNameIdentifierOwner 17 | import org.jetbrains.kotlin.psi.KtClass 18 | import org.jetbrains.kotlin.psi.KtElement 19 | import org.jetbrains.kotlin.psi.KtFile 20 | import org.jetbrains.kotlin.psi.KtFunction 21 | 22 | abstract class DetektRule(config: Config = Config.empty) : 23 | Rule(config), 24 | ComposeKtVisitor { 25 | 26 | private val config: DetektComposeKtConfig by lazy { DetektComposeKtConfig(this) } 27 | 28 | private val emitter: Emitter = Emitter { element, message, canBeAutoCorrected -> 29 | // Grab the named element if there were any, otherwise fall back to the whole PsiElement 30 | val finalElement = element.runIf(element is PsiNameIdentifierOwner) { 31 | (this as PsiNameIdentifierOwner).nameIdentifier!! 32 | } 33 | val finding = when { 34 | canBeAutoCorrected -> CorrectableCodeSmell( 35 | issue = issue, 36 | entity = Entity.from(finalElement, Location.from(finalElement)), 37 | message = message, 38 | autoCorrectEnabled = autoCorrect, 39 | ) 40 | 41 | else -> CodeSmell( 42 | issue = issue, 43 | entity = Entity.from(finalElement, Location.from(finalElement)), 44 | message = message, 45 | ) 46 | } 47 | report(finding) 48 | 49 | when { 50 | this@DetektRule.autoCorrect && canBeAutoCorrected -> Decision.Fix 51 | else -> Decision.Ignore 52 | } 53 | } 54 | 55 | override fun visit(root: KtFile) { 56 | super.visit(root) 57 | visitFile(root, emitter, config) 58 | } 59 | 60 | override fun visitClass(klass: KtClass) { 61 | super.visitClass(klass) 62 | visitClass(klass, emitter, config) 63 | } 64 | 65 | override fun visitKtElement(element: KtElement) { 66 | super.visitKtElement(element) 67 | when (element) { 68 | is KtFunction -> { 69 | visitFunction(element, emitter, config) 70 | if (element.isComposable) { 71 | visitComposable(element, emitter, config) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ComposableAnnotationNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.ComposableAnnotationNaming 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class ComposableAnnotationNamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ComposableAnnotationNaming() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "ComposableAnnotationNaming", 19 | severity = Severity.CodeSmell, 20 | description = ComposableAnnotationNaming.ComposableAnnotationDoesNotEndWithComposable, 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ComposeRuleSetProvider.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.RuleSet 7 | import io.gitlab.arturbosch.detekt.api.RuleSetProvider 8 | 9 | class ComposeRuleSetProvider : RuleSetProvider { 10 | override val ruleSetId: String = CUSTOM_RULE_SET_ID 11 | 12 | override fun instance(config: Config): RuleSet = RuleSet( 13 | CUSTOM_RULE_SET_ID, 14 | listOf( 15 | ComposableAnnotationNamingCheck(config), 16 | CompositionLocalAllowlistCheck(config), 17 | CompositionLocalNamingCheck(config), 18 | ContentEmitterReturningValuesCheck(config), 19 | ContentSlotReusedCheck(config), 20 | ContentTrailingLambdaCheck(config), 21 | DefaultsVisibilityCheck(config), 22 | LambdaParameterEventTrailingCheck(config), 23 | LambdaParameterInRestartableEffectCheck(config), 24 | Material2Check(config), 25 | ModifierClickableOrderCheck(config), 26 | ModifierComposedCheck(config), 27 | ModifierMissingCheck(config), 28 | ModifierNamingCheck(config), 29 | ModifierNotUsedAtRootCheck(config), 30 | ModifierReusedCheck(config), 31 | ModifierWithoutDefaultCheck(config), 32 | MultipleContentEmittersCheck(config), 33 | MutableParametersCheck(config), 34 | MutableStateAutoboxingCheck(config), 35 | MutableStateParameterCheck(config), 36 | NamingCheck(config), 37 | ParameterNamingCheck(config), 38 | ParameterOrderCheck(config), 39 | PreviewAnnotationNamingCheck(config), 40 | PreviewNamingCheck(config), 41 | PreviewPublicCheck(config), 42 | RememberContentMissingCheck(config), 43 | RememberStateMissingCheck(config), 44 | UnstableCollectionsCheck(config), 45 | ViewModelForwardingCheck(config), 46 | ViewModelInjectionCheck(config), 47 | ), 48 | ) 49 | 50 | private companion object { 51 | const val CUSTOM_RULE_SET_ID = "Compose" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/CompositionLocalAllowlistCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.CompositionLocalAllowlist 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class CompositionLocalAllowlistCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by CompositionLocalAllowlist() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "CompositionLocalAllowlist", 19 | severity = Severity.CodeSmell, 20 | description = CompositionLocalAllowlist.CompositionLocalNotInAllowlist, 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/CompositionLocalNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.CompositionLocalNaming 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class CompositionLocalNamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by CompositionLocalNaming() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "CompositionLocalNaming", 19 | severity = Severity.CodeSmell, 20 | description = CompositionLocalNaming.CompositionLocalNeedsLocalPrefix, 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ContentEmitterReturningValuesCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.ContentEmitterReturningValues 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class ContentEmitterReturningValuesCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ContentEmitterReturningValues() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "ContentEmitterReturningValues", 19 | severity = Severity.Defect, 20 | description = ContentEmitterReturningValues.ContentEmitterReturningValuesToo, 21 | debt = Debt.TWENTY_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ContentSlotReusedCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.ContentSlotReused 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class ContentSlotReusedCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ContentSlotReused() { 16 | override val issue: Issue = Issue( 17 | id = "ContentSlotReused", 18 | severity = Severity.Defect, 19 | description = ContentSlotReused.ContentSlotsShouldNotBeReused, 20 | debt = Debt.TEN_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ContentTrailingLambdaCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.ContentTrailingLambda 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class ContentTrailingLambdaCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ContentTrailingLambda() { 16 | override val issue: Issue = Issue( 17 | id = "ContentTrailingLambda", 18 | severity = Severity.CodeSmell, 19 | description = ContentTrailingLambda.ContentShouldBeTrailingLambda, 20 | debt = Debt.TEN_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/DefaultsVisibilityCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DefaultsVisibility 11 | import io.nlopez.compose.rules.DetektRule 12 | 13 | class DefaultsVisibilityCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by DefaultsVisibility() { 16 | override val issue: Issue = Issue( 17 | id = "DefaultsVisibility", 18 | severity = Severity.Defect, 19 | description = "@Composable `Defaults` objects should match visibility of the composables they serve.", 20 | debt = Debt.TEN_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/LambdaParameterEventTrailingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.LambdaParameterEventTrailing 12 | 13 | class LambdaParameterEventTrailingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by LambdaParameterEventTrailing() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "LambdaParameterEventTrailing", 19 | severity = Severity.Style, 20 | description = LambdaParameterEventTrailing.EventLambdaIsTrailingLambda, 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/LambdaParameterInRestartableEffectCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.LambdaParameterInRestartableEffect 12 | 13 | class LambdaParameterInRestartableEffectCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by LambdaParameterInRestartableEffect() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "LambdaParameterInRestartableEffect", 19 | severity = Severity.Defect, 20 | description = LambdaParameterInRestartableEffect.LambdaUsedInRestartableEffect, 21 | debt = Debt.TWENTY_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/Material2Check.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.Material2 12 | 13 | class Material2Check(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by Material2() { 16 | override val issue: Issue = Issue( 17 | id = "Material2", 18 | severity = Severity.Maintainability, 19 | description = Material2.DisallowedUsageOfMaterial2, 20 | debt = Debt.TEN_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierClickableOrderCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ModifierClickableOrder 12 | 13 | class ModifierClickableOrderCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ModifierClickableOrder() { 16 | override val issue: Issue = Issue( 17 | id = "ModifierClickableOrder", 18 | severity = Severity.Defect, 19 | description = ModifierClickableOrder.ModifierChainWithSuspiciousOrder, 20 | debt = Debt.FIVE_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierComposedCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ModifierComposed 12 | 13 | class ModifierComposedCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ModifierComposed() { 16 | override val issue: Issue = Issue( 17 | id = "ModifierComposed", 18 | severity = Severity.Performance, 19 | description = ModifierComposed.ComposedModifier, 20 | debt = Debt.TEN_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierMissingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.gitlab.arturbosch.detekt.api.config 10 | import io.nlopez.compose.core.ComposeKtConfig 11 | import io.nlopez.compose.core.ComposeKtVisitor 12 | import io.nlopez.compose.core.Emitter 13 | import io.nlopez.compose.core.util.isAnnotatedWith 14 | import io.nlopez.compose.rules.DetektRule 15 | import io.nlopez.compose.rules.ModifierMissing 16 | import org.jetbrains.kotlin.psi.KtFunction 17 | 18 | class ModifierMissingCheck(config: Config) : DetektRule(config) { 19 | override val issue: Issue = Issue( 20 | id = "ModifierMissing", 21 | severity = Severity.Defect, 22 | description = ModifierMissing.MissingModifierContentComposable, 23 | debt = Debt.TEN_MINS, 24 | ) 25 | private val visitor: ComposeKtVisitor = ModifierMissing() 26 | 27 | // On the docs it looks like this is a common suppressor that should be available everywhere, 28 | // but it doesn't seem to be (according to unit tests). Oh well, I guess I'll just leave the extra check for now. 29 | private val ignoreAnnotated: List by config(emptyList()) { list -> list.map(String::trim) } 30 | 31 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 32 | if (function.isAnnotatedWith(ignoreAnnotated.toSet())) return 33 | visitor.visitComposable(function, emitter, config) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ModifierNaming 12 | 13 | class ModifierNamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ModifierNaming() { 16 | override val issue: Issue = Issue( 17 | id = "ModifierNaming", 18 | severity = Severity.CodeSmell, 19 | description = ModifierNaming.ModifiersAreSupposedToBeCalledModifierWhenAlone, 20 | debt = Debt.FIVE_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierNotUsedAtRootCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ModifierNotUsedAtRoot 12 | 13 | class ModifierNotUsedAtRootCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ModifierNotUsedAtRoot() { 16 | override val issue: Issue = Issue( 17 | id = "ModifierNotUsedAtRoot", 18 | severity = Severity.Defect, 19 | description = ModifierNotUsedAtRoot.ComposableModifierShouldBeUsedAtTheTopMostPossiblePlace, 20 | debt = Debt.FIVE_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierReusedCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ModifierReused 12 | 13 | class ModifierReusedCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ModifierReused() { 16 | override val issue: Issue = Issue( 17 | id = "ModifierReused", 18 | severity = Severity.Defect, 19 | description = ModifierReused.ModifierShouldBeUsedOnceOnly, 20 | debt = Debt.TWENTY_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ModifierWithoutDefaultCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ModifierWithoutDefault 12 | 13 | class ModifierWithoutDefaultCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ModifierWithoutDefault() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "ModifierWithoutDefault", 19 | severity = Severity.CodeSmell, 20 | description = ModifierWithoutDefault.MissingModifierDefaultParam, 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/MultipleContentEmittersCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.MultipleContentEmitters 12 | 13 | class MultipleContentEmittersCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by MultipleContentEmitters() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "MultipleEmitters", 19 | severity = Severity.Defect, 20 | description = MultipleContentEmitters.MultipleContentEmittersDetected, 21 | debt = Debt.TWENTY_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/MutableParametersCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.MutableParameters 12 | 13 | class MutableParametersCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by MutableParameters() { 16 | override val issue: Issue = Issue( 17 | id = "MutableParams", 18 | severity = Severity.Defect, 19 | description = MutableParameters.MutableParameterInCompose, 20 | debt = Debt.TWENTY_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/MutableStateAutoboxingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.MutableStateAutoboxing 12 | 13 | class MutableStateAutoboxingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by MutableStateAutoboxing() { 16 | override val issue: Issue = Issue( 17 | id = "MutableStateAutoboxing", 18 | severity = Severity.Performance, 19 | description = "Using mutableInt/Long/Double/FloatStateOf is recommended over mutableStateOf for " + 20 | "Int/Long/Double/Float, as it uses the primitives directly which is more performant.", 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/MutableStateParameterCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.MutableStateParameter 12 | 13 | class MutableStateParameterCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by MutableStateParameter() { 16 | override val issue: Issue = Issue( 17 | id = "MutableStateParam", 18 | severity = Severity.Defect, 19 | description = MutableStateParameter.MutableStateParameterInCompose, 20 | debt = Debt.TWENTY_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/NamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.Naming 12 | 13 | class NamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by Naming() { 16 | override val issue: Issue = Issue( 17 | id = "ComposableNaming", 18 | severity = Severity.CodeSmell, 19 | description = """ 20 | Composable functions that return Unit should start with an uppercase letter. They are considered declarative entities that can be either present or absent in a composition and therefore follow the naming rules for classes. 21 | 22 | However, Composable functions that return a value should start with a lowercase letter instead. They should follow the standard Kotlin Coding Conventions for the naming of functions for any function annotated @Composable that returns a value other than Unit 23 | """.trimIndent(), 24 | debt = Debt.TEN_MINS, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ParameterNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ParameterNaming 12 | 13 | class ParameterNamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ParameterNaming() { 16 | override val issue: Issue = Issue( 17 | id = "ParameterNaming", 18 | severity = Severity.CodeSmell, 19 | description = """ 20 | Lambda parameters in a composable function should be in present tense, not past tense. 21 | 22 | Examples: `onClick` and not `onClicked`, `onTextChange` and not `onTextChanged`, etc. 23 | """.trimIndent(), 24 | debt = Debt.FIVE_MINS, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ParameterOrderCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ParameterOrder 12 | 13 | class ParameterOrderCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ParameterOrder() { 16 | override val issue: Issue = Issue( 17 | id = "ComposableParamOrder", 18 | severity = Severity.CodeSmell, 19 | description = "Parameters in a composable function should be ordered following this pattern: " + 20 | "params without defaults, modifiers, params with defaults and optionally, " + 21 | "a trailing function that might not have a default param.", 22 | debt = Debt.TEN_MINS, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/PreviewAnnotationNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.PreviewAnnotationNaming 12 | 13 | class PreviewAnnotationNamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by PreviewAnnotationNaming() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "PreviewAnnotationNaming", 19 | severity = Severity.CodeSmell, 20 | description = "Multipreview annotations should begin with the `Preview` suffix", 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/PreviewNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.PreviewNaming 12 | 13 | class PreviewNamingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by PreviewNaming() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "PreviewNaming", 19 | severity = Severity.Style, 20 | description = "Enforces a cohesive naming strategy for preview @Composable functions.", 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/PreviewPublicCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.PreviewPublic 12 | 13 | class PreviewPublicCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by PreviewPublic() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "PreviewPublic", 19 | severity = Severity.CodeSmell, 20 | description = PreviewPublic.ComposablesPreviewShouldNotBePublic, 21 | debt = Debt.FIVE_MINS, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/RememberContentMissingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.RememberContentMissing 12 | 13 | class RememberContentMissingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by RememberContentMissing() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "RememberContentMissing", 19 | severity = Severity.Defect, 20 | description = """ 21 | Using movableContentOf/movableContentWithReceiverOf in a @Composable function without it being remembered can cause visual problems, as the content would be recycled when detached from the composition. 22 | """.trimIndent(), 23 | debt = Debt.FIVE_MINS, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/RememberStateMissingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.RememberStateMissing 12 | 13 | class RememberStateMissingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by RememberStateMissing() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "RememberMissing", 19 | severity = Severity.Defect, 20 | description = """ 21 | Using mutableStateOf/derivedStateOf in a @Composable function without it being inside of a remember function. 22 | If you don't remember the state instance, a new state instance will be created when the function is recomposed. 23 | """.trimIndent(), 24 | debt = Debt.FIVE_MINS, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/UnstableCollectionsCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.UnstableCollections 12 | 13 | class UnstableCollectionsCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by UnstableCollections() { 16 | override val issue: Issue = Issue( 17 | id = "UnstableCollections", 18 | severity = Severity.Defect, 19 | description = """ 20 | The Compose Compiler cannot infer the stability of a parameter if a List/Set/Map is used in it, even if the item type is stable. 21 | You should use Kotlinx Immutable Collections instead, or create an `@Immutable` wrapper for this class. 22 | 23 | See https://mrmans0n.github.io/compose-rules/rules/#avoid-using-unstable-collections for more information. 24 | """.trimIndent(), 25 | debt = Debt.TWENTY_MINS, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ViewModelForwardingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ViewModelForwarding 12 | 13 | class ViewModelForwardingCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ViewModelForwarding() { 16 | override val issue: Issue = Issue( 17 | id = "ViewModelForwarding", 18 | severity = Severity.CodeSmell, 19 | description = ViewModelForwarding.AvoidViewModelForwarding, 20 | debt = Debt.TWENTY_MINS, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/ViewModelInjectionCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.Debt 7 | import io.gitlab.arturbosch.detekt.api.Issue 8 | import io.gitlab.arturbosch.detekt.api.Severity 9 | import io.nlopez.compose.core.ComposeKtVisitor 10 | import io.nlopez.compose.rules.DetektRule 11 | import io.nlopez.compose.rules.ViewModelInjection 12 | 13 | class ViewModelInjectionCheck(config: Config) : 14 | DetektRule(config), 15 | ComposeKtVisitor by ViewModelInjection() { 16 | 17 | override val issue: Issue = Issue( 18 | id = "ViewModelInjection", 19 | severity = Severity.CodeSmell, 20 | description = """ 21 | Implicit dependencies of composables should be made explicit. 22 | 23 | Acquiring a ViewModel should be done in composable default parameters, so that it is more testable and flexible. 24 | """.trimIndent(), 25 | debt = Debt.TEN_MINS, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /rules/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider: -------------------------------------------------------------------------------- 1 | io.nlopez.compose.rules.detekt.ComposeRuleSetProvider 2 | -------------------------------------------------------------------------------- /rules/detekt/src/main/resources/config/config.yml: -------------------------------------------------------------------------------- 1 | Compose: 2 | ComposableAnnotationNaming: 3 | active: true 4 | ComposableNaming: 5 | active: true 6 | ComposableParamOrder: 7 | active: true 8 | CompositionLocalAllowlist: 9 | active: true 10 | CompositionLocalNaming: 11 | active: true 12 | ContentEmitterReturningValues: 13 | active: true 14 | ContentSlotReused: 15 | active: true 16 | ContentTrailingLambda: 17 | active: true 18 | DefaultsVisibility: 19 | active: true 20 | LambdaParameterEventTrailing: 21 | active: true 22 | LambdaParameterInRestartableEffect: 23 | active: true 24 | Material2: 25 | active: false 26 | ModifierClickableOrder: 27 | active: true 28 | ModifierComposed: 29 | active: true 30 | ModifierMissing: 31 | active: true 32 | ModifierNaming: 33 | active: true 34 | ModifierNotUsedAtRoot: 35 | active: true 36 | ModifierReused: 37 | active: true 38 | ModifierWithoutDefault: 39 | active: true 40 | MultipleEmitters: 41 | active: true 42 | MutableParams: 43 | active: true 44 | MutableStateAutoboxing: 45 | active: true 46 | MutableStateParam: 47 | active: true 48 | ParameterNaming: 49 | active: true 50 | PreviewAnnotationNaming: 51 | active: true 52 | PreviewNaming: 53 | active: false 54 | PreviewPublic: 55 | active: true 56 | RememberContentMissing: 57 | active: true 58 | RememberMissing: 59 | active: true 60 | UnstableCollections: 61 | active: false 62 | ViewModelForwarding: 63 | active: true 64 | ViewModelInjection: 65 | active: true 66 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/DetektComposeKtConfigTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import io.gitlab.arturbosch.detekt.test.TestConfig 6 | import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat 7 | import org.junit.jupiter.api.Test 8 | 9 | class DetektComposeKtConfigTest { 10 | private val detektConfig = TestConfig( 11 | "myInt" to 10, 12 | "myString" to "abcd", 13 | "myList" to "a,b,c,a", 14 | "myList2" to "a , b , c,a", 15 | "mySet" to "a,b,c,a,b,c", 16 | "mySet2" to " a, b,c ,a , b , c ", 17 | "myBool" to true, 18 | ) 19 | private val config = DetektComposeKtConfig(detektConfig) 20 | 21 | @Test 22 | fun `returns ints from Config, and default values when unset`() { 23 | assertThat(config.getInt("myInt", 0)).isEqualTo(10) 24 | assertThat(config.getInt("myOtherInt", 0)).isEqualTo(0) 25 | } 26 | 27 | @Test 28 | fun `returns strings from Config, and default values when unset`() { 29 | assertThat(config.getString("myString", null)).isEqualTo("abcd") 30 | assertThat(config.getString("myOtherString", "ABCD")).isEqualTo("ABCD") 31 | assertThat(config.getString("myOtherStringWithNullDefault", null)).isNull() 32 | } 33 | 34 | @Test 35 | fun `returns lists from Config, and default values when unset`() { 36 | assertThat(config.getList("myList", emptyList())).containsExactly("a", "b", "c", "a") 37 | assertThat(config.getList("myList2", emptyList())).containsExactly("a", "b", "c", "a") 38 | assertThat(config.getList("myOtherList", listOf("a"))).containsExactly("a") 39 | } 40 | 41 | @Test 42 | fun `returns sets from Config, and default values when unset`() { 43 | assertThat(config.getSet("mySet", emptySet())).containsExactly("a", "b", "c") 44 | assertThat(config.getSet("mySet2", emptySet())).containsExactly("a", "b", "c") 45 | assertThat(config.getSet("myOtherSet", setOf("a"))).containsExactly("a") 46 | } 47 | 48 | @Test 49 | fun `returns booleans from Config, and default values when unset`() { 50 | assertThat(config.getBoolean("myBool", false)).isTrue() 51 | assertThat(config.getBoolean("myOtherBool", false)).isFalse() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/ComposableAnnotationNamingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.ComposableAnnotationNaming 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class ComposableAnnotationNamingCheckTest { 14 | 15 | private val rule = ComposableAnnotationNamingCheck(Config.empty) 16 | 17 | @Test 18 | fun `passes for non-composable annotations`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | annotation class Banana 23 | """.trimIndent() 24 | val errors = rule.lint(code) 25 | assertThat(errors).isEmpty() 26 | } 27 | 28 | @Test 29 | fun `passes for composable annotations with the proper names`() { 30 | @Language("kotlin") 31 | val code = 32 | """ 33 | @ComposableTargetMarker 34 | annotation class BananaComposable 35 | @ComposableTargetMarker 36 | annotation class AppleComposable 37 | """.trimIndent() 38 | val errors = rule.lint(code) 39 | assertThat(errors).isEmpty() 40 | } 41 | 42 | @Test 43 | fun `errors when a composable annotation is not correctly named`() { 44 | @Language("kotlin") 45 | val code = 46 | """ 47 | @ComposableTargetMarker 48 | annotation class Banana 49 | @ComposableTargetMarker 50 | annotation class Apple 51 | """.trimIndent() 52 | val errors = rule.lint(code) 53 | assertThat(errors).hasStartSourceLocations( 54 | SourceLocation(2, 18), 55 | SourceLocation(4, 18), 56 | ) 57 | for (error in errors) { 58 | assertThat(error) 59 | .hasMessage(ComposableAnnotationNaming.ComposableAnnotationDoesNotEndWithComposable) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/CompositionLocalAllowlistCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.SourceLocation 6 | import io.gitlab.arturbosch.detekt.test.TestConfig 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.CompositionLocalAllowlist 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class CompositionLocalAllowlistCheckTest { 14 | 15 | private val testConfig = TestConfig( 16 | "allowedCompositionLocals" to listOf("LocalBanana", "LocalPotato"), 17 | ) 18 | private val rule = CompositionLocalAllowlistCheck(testConfig) 19 | 20 | @Test 21 | fun `error when a CompositionLocal is defined`() { 22 | @Language("kotlin") 23 | val code = 24 | """ 25 | private val LocalApple = staticCompositionLocalOf { "Apple" } 26 | internal val LocalPlum: String = staticCompositionLocalOf { "Plum" } 27 | val LocalPrune = compositionLocalOf { "Prune" } 28 | private val LocalKiwi: String = compositionLocalOf { "Kiwi" } 29 | """.trimIndent() 30 | val errors = rule.lint(code) 31 | assertThat(errors) 32 | .hasStartSourceLocations( 33 | SourceLocation(1, 13), 34 | SourceLocation(2, 14), 35 | SourceLocation(3, 5), 36 | SourceLocation(4, 13), 37 | ) 38 | for (error in errors) { 39 | assertThat(error).hasMessage(CompositionLocalAllowlist.CompositionLocalNotInAllowlist) 40 | } 41 | } 42 | 43 | @Test 44 | fun `passes when a CompositionLocal is defined but it's in the allowlist`() { 45 | @Language("kotlin") 46 | val code = 47 | """ 48 | val LocalBanana = staticCompositionLocalOf { "Banana" } 49 | val LocalPotato = compositionLocalOf { "Potato" } 50 | """.trimIndent() 51 | val errors = rule.lint(code) 52 | assertThat(errors).isEmpty() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/CompositionLocalNamingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.CompositionLocalNaming 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class CompositionLocalNamingCheckTest { 14 | 15 | private val rule = CompositionLocalNamingCheck(Config.empty) 16 | 17 | @Test 18 | fun `error when a CompositionLocal has a wrong name`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | val AppleLocal = staticCompositionLocalOf { "Apple" } 23 | val Plum: String = staticCompositionLocalOf { "Plum" } 24 | """.trimIndent() 25 | val errors = rule.lint(code) 26 | assertThat(errors) 27 | .hasStartSourceLocations( 28 | SourceLocation(1, 5), 29 | SourceLocation(2, 5), 30 | ) 31 | for (error in errors) { 32 | assertThat(error).hasMessage(CompositionLocalNaming.CompositionLocalNeedsLocalPrefix) 33 | } 34 | } 35 | 36 | @Test 37 | fun `passes when a CompositionLocal is well named`() { 38 | @Language("kotlin") 39 | val code = 40 | """ 41 | val LocalBanana = staticCompositionLocalOf { "Banana" } 42 | val LocalPotato = compositionLocalOf { "Potato" } 43 | """.trimIndent() 44 | val errors = rule.lint(code) 45 | assertThat(errors).isEmpty() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/ContentEmitterReturningValuesCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.SourceLocation 6 | import io.gitlab.arturbosch.detekt.test.TestConfig 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.ContentEmitterReturningValues 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class ContentEmitterReturningValuesCheckTest { 14 | 15 | private val testConfig = TestConfig( 16 | "contentEmitters" to listOf("Potato", "Banana"), 17 | ) 18 | private val rule = ContentEmitterReturningValuesCheck(testConfig) 19 | 20 | @Test 21 | fun `error out when detecting a content emitting composable that returns something other than unit`() { 22 | @Language("kotlin") 23 | val code = 24 | """ 25 | @Composable 26 | fun Something(): String { // This one emits content directly and should fail 27 | Text("Hi") 28 | return "Potato" 29 | } 30 | @Composable 31 | fun Something2(): WhateverState { // This one emits content indirectly and should fail too 32 | Something3() 33 | return remember { WhateverState() } 34 | } 35 | @Composable 36 | fun Something3() { // This one is fine but calling it should make Something2 fail 37 | Potato(icon = HorizonIcon.Arrow) 38 | } 39 | @Composable 40 | fun Something4(): String { // This one is using a composable defined in the config 41 | Banana() 42 | } 43 | """.trimIndent() 44 | val errors = rule.lint(code) 45 | assertThat(errors) 46 | .hasStartSourceLocations( 47 | SourceLocation(2, 5), 48 | SourceLocation(7, 5), 49 | SourceLocation(16, 5), 50 | ) 51 | for (error in errors) { 52 | assertThat(error).hasMessage(ContentEmitterReturningValues.ContentEmitterReturningValuesToo) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/DefaultsVisibilityCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.DefaultsVisibility.Companion.createMessage 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class DefaultsVisibilityCheckTest { 14 | 15 | private val rule = DefaultsVisibilityCheck(Config.empty) 16 | 17 | @Test 18 | fun `errors when a defaults object has less visibility than the composable that uses it`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | internal object MyComposableDefaults 23 | @Composable 24 | fun MyComposable(someParam: Bleh = MyComposableDefaults.someParam) { } 25 | private object MyOtherComposableDefaults 26 | @Composable 27 | internal fun MyOtherComposable() { 28 | val someUsage = MyOtherComposableDefaults.someParam.someMethod() 29 | } 30 | """.trimIndent() 31 | val errors = rule.lint(code) 32 | assertThat(errors) 33 | .hasStartSourceLocations( 34 | SourceLocation(1, 17), 35 | SourceLocation(4, 16), 36 | ) 37 | assertThat(errors[0]).hasMessage( 38 | createMessage("public", "MyComposableDefaults", "internal"), 39 | ) 40 | assertThat(errors[1]).hasMessage( 41 | createMessage("internal", "MyOtherComposableDefaults", "private"), 42 | ) 43 | } 44 | 45 | @Test 46 | fun `passes when a defaults object has the same visibility as any of the overloaded composables that match it`() { 47 | @Language("kotlin") 48 | val code = 49 | """ 50 | object MyComposableDefaults 51 | @Composable 52 | fun MyComposable(someParam: Bleh = MyComposableDefaults.someParam) { } 53 | internal object MyOtherComposableDefaults 54 | @Composable 55 | internal fun MyOtherComposable() { 56 | val someUsage = MyOtherComposableDefaults.someParam.someMethod() 57 | } 58 | object MyThirdComposableDefaults 59 | @Composable 60 | fun MyThirdComposable(a: A) { 61 | val someUsage = MyThirdComposableDefaults.someParam 62 | } 63 | @Composable 64 | internal fun MyThirdComposable(b: B) { 65 | val someUsage = MyThirdComposableDefaults.someParam 66 | } 67 | """.trimIndent() 68 | val errors = rule.lint(code) 69 | assertThat(errors).isEmpty() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/LambdaParameterEventTrailingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.LambdaParameterEventTrailing 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class LambdaParameterEventTrailingCheckTest { 14 | 15 | private val rule = LambdaParameterEventTrailingCheck(Config.empty) 16 | 17 | @Test 18 | fun `error out when detecting a lambda being as trailing`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | @Composable 23 | fun Something(modifier: Modifier = Modifier, onClick: () -> Unit) { 24 | Text("Hello") 25 | } 26 | """.trimIndent() 27 | val errors = rule.lint(code) 28 | assertThat(errors) 29 | .hasStartSourceLocations( 30 | SourceLocation(2, 46), 31 | ) 32 | for (error in errors) { 33 | assertThat(error).hasMessage(LambdaParameterEventTrailing.EventLambdaIsTrailingLambda) 34 | } 35 | } 36 | 37 | @Test 38 | fun `passes when a lambda is required`() { 39 | @Language("kotlin") 40 | val code = 41 | """ 42 | @Composable 43 | fun Something(onClick: () -> Unit, modifier: Modifier = Modifier) { 44 | Text("Hello") 45 | } 46 | """.trimIndent() 47 | val errors = rule.lint(code) 48 | assertThat(errors).isEmpty() 49 | } 50 | 51 | @Test 52 | fun `passes when a lambda is composable`() { 53 | @Language("kotlin") 54 | val code = 55 | """ 56 | @Composable 57 | fun Something(modifier: Modifier = Modifier, on: @Composable () -> Unit) { 58 | Text("Hello") 59 | } 60 | """.trimIndent() 61 | val errors = rule.lint(code) 62 | assertThat(errors).isEmpty() 63 | } 64 | 65 | @Test 66 | fun `passes when the function doesnt emit content`() { 67 | @Language("kotlin") 68 | val code = 69 | """ 70 | @Composable 71 | fun something(onClick: () -> Unit) {} 72 | """.trimIndent() 73 | val errors = rule.lint(code) 74 | assertThat(errors).isEmpty() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/ModifierComposedCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.test.assertThat 7 | import io.gitlab.arturbosch.detekt.test.lint 8 | import io.nlopez.compose.rules.ModifierComposed 9 | import org.intellij.lang.annotations.Language 10 | import org.junit.jupiter.api.Test 11 | 12 | class ModifierComposedCheckTest { 13 | 14 | private val rule = ModifierComposedCheck(Config.empty) 15 | 16 | @Test 17 | fun `errors when a composed Modifier extension is detected`() { 18 | @Language("kotlin") 19 | val code = 20 | """ 21 | fun Modifier.something1(): Modifier = composed {} 22 | fun Modifier.something2() = composed {} 23 | fun Modifier.something3() { 24 | return composed {} 25 | } 26 | """.trimIndent() 27 | val errors = rule.lint(code) 28 | assertThat(errors).hasTextLocations("something1", "something2", "something3") 29 | for (error in errors) { 30 | assertThat(error).hasMessage(ModifierComposed.ComposedModifier) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/ModifierNamingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.ModifierNaming 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class ModifierNamingCheckTest { 14 | 15 | private val rule = ModifierNamingCheck(Config.empty) 16 | 17 | @Test 18 | fun `errors when a Composable has a modifier not named modifier`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | @Composable 23 | fun Something1(m: Modifier) {} 24 | @Composable 25 | fun Something2(m: Modifier, m2: Modifier) {} 26 | """.trimIndent() 27 | 28 | val errors = rule.lint(code) 29 | assertThat(errors) 30 | .hasStartSourceLocations( 31 | SourceLocation(2, 16), 32 | SourceLocation(4, 16), 33 | SourceLocation(4, 29), 34 | ) 35 | 36 | assertThat(errors[0]).hasMessage(ModifierNaming.ModifiersAreSupposedToBeCalledModifierWhenAlone) 37 | assertThat(errors[1]).hasMessage(ModifierNaming.ModifiersAreSupposedToEndInModifierWhenMultiple) 38 | assertThat(errors[2]).hasMessage(ModifierNaming.ModifiersAreSupposedToEndInModifierWhenMultiple) 39 | } 40 | 41 | @Test 42 | fun `passes when the modifiers are named correctly`() { 43 | @Language("kotlin") 44 | val code = 45 | """ 46 | @Composable 47 | fun Something1(modifier: Modifier) {} 48 | @Composable 49 | fun Something2(modifier: Modifier, otherModifier: Modifier) {} 50 | """.trimIndent() 51 | 52 | val errors = rule.lint(code) 53 | assertThat(errors).isEmpty() 54 | } 55 | 56 | @Test 57 | fun `errors when a Composable has a single modifier not named modifier but ends with modifier`() { 58 | @Language("kotlin") 59 | val code = 60 | """ 61 | @Composable 62 | fun Something1(myModifier: Modifier) {} 63 | """.trimIndent() 64 | 65 | val errors = rule.lint(code) 66 | assertThat(errors).hasStartSourceLocation(2, 16) 67 | 68 | assertThat(errors[0]).hasMessage(ModifierNaming.ModifiersAreSupposedToBeCalledModifierWhenAlone) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/MutableParametersCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.MutableParameters 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class MutableParametersCheckTest { 14 | 15 | private val rule = MutableParametersCheck(Config.empty) 16 | 17 | @Test 18 | fun `errors when a Composable has a mutable parameter`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | @Composable 23 | fun Something(a: ArrayList) {} 24 | @Composable 25 | fun Something(a: HashSet) {} 26 | @Composable 27 | fun Something(a: MutableMap) {} 28 | """.trimIndent() 29 | val errors = rule.lint(code) 30 | assertThat(errors) 31 | .hasStartSourceLocations( 32 | SourceLocation(2, 15), 33 | SourceLocation(4, 15), 34 | SourceLocation(6, 15), 35 | ) 36 | for (error in errors) { 37 | assertThat(error).hasMessage(MutableParameters.MutableParameterInCompose) 38 | } 39 | } 40 | 41 | @Test 42 | fun `no errors when a Composable has valid parameters`() { 43 | @Language("kotlin") 44 | val code = 45 | """ 46 | @Composable 47 | fun Something(a: String, b: (Int) -> Unit) {} 48 | @Composable 49 | fun Something(a: State) {} 50 | """.trimIndent() 51 | val errors = rule.lint(code) 52 | assertThat(errors).isEmpty() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/MutableStateParameterCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.MutableStateParameter 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class MutableStateParameterCheckTest { 14 | 15 | private val rule = MutableStateParameterCheck(Config.empty) 16 | 17 | @Test 18 | fun `errors when a Composable has a MutableState parameter`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | @Composable 23 | fun Something(a: MutableState) {} 24 | """.trimIndent() 25 | val errors = rule.lint(code) 26 | assertThat(errors) 27 | .hasStartSourceLocations( 28 | SourceLocation(2, 15), 29 | ) 30 | for (error in errors) { 31 | assertThat(error).hasMessage(MutableStateParameter.MutableStateParameterInCompose) 32 | } 33 | } 34 | 35 | @Test 36 | fun `no errors when a Composable has valid parameters`() { 37 | @Language("kotlin") 38 | val code = 39 | """ 40 | @Composable 41 | fun Something(a: String, b: (Int) -> Unit) {} 42 | @Composable 43 | fun Something(a: State) {} 44 | """.trimIndent() 45 | val errors = rule.lint(code) 46 | assertThat(errors).isEmpty() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/PreviewPublicCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.PreviewPublic 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class PreviewPublicCheckTest { 14 | 15 | private val rule = PreviewPublicCheck(Config.empty) 16 | 17 | @Test 18 | fun `passes for non-preview public composables`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | @Composable 23 | fun MyComposable() { } 24 | """.trimIndent() 25 | val errors = rule.lint(code) 26 | assertThat(errors).isEmpty() 27 | } 28 | 29 | @Test 30 | fun `errors for preview public composables`() { 31 | @Language("kotlin") 32 | val code = 33 | """ 34 | @Preview 35 | @Composable 36 | fun MyComposable() { } 37 | @CombinedPreviews 38 | @Composable 39 | fun MyComposable() { } 40 | """.trimIndent() 41 | val errors = rule.lint(code) 42 | assertThat(errors).hasStartSourceLocations( 43 | SourceLocation(3, 5), 44 | SourceLocation(6, 5), 45 | ) 46 | for (error in errors) { 47 | assertThat(error).hasMessage(PreviewPublic.ComposablesPreviewShouldNotBePublic) 48 | } 49 | } 50 | 51 | @Test 52 | fun `passes when a non-public preview composable uses preview params`() { 53 | @Language("kotlin") 54 | val code = 55 | """ 56 | @Preview 57 | @Composable 58 | private fun MyComposable(user: User) { 59 | } 60 | @CombinedPreviews 61 | @Composable 62 | internal fun MyComposable(user: User) { 63 | } 64 | """.trimIndent() 65 | val errors = rule.lint(code) 66 | assertThat(errors).isEmpty() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/RememberContentMissingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.RememberContentMissing 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class RememberContentMissingCheckTest { 14 | 15 | private val rule = RememberContentMissingCheck(Config.empty) 16 | 17 | @Test 18 | fun `passes when a non-remembered movableContentOf is used outside of a Composable`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | val mco = movableContentOf { Text("X") } 23 | """.trimIndent() 24 | val errors = rule.lint(code) 25 | assertThat(errors).isEmpty() 26 | } 27 | 28 | @Test 29 | fun `errors when a non-remembered movableContentOf is used in a Composable`() { 30 | @Language("kotlin") 31 | val code = 32 | """ 33 | @Composable 34 | fun MyComposable() { 35 | val something = movableContentOf { Text("X") } 36 | } 37 | 38 | """.trimIndent() 39 | val errors = rule.lint(code) 40 | assertThat(errors) 41 | .hasStartSourceLocations( 42 | SourceLocation(3, 21), 43 | ) 44 | for (error in errors) { 45 | assertThat(error).hasMessage(RememberContentMissing.MovableContentOfNotRemembered) 46 | } 47 | } 48 | 49 | @Test 50 | fun `errors when a non-remembered movableContentWithReceiverOf is used in a Composable`() { 51 | @Language("kotlin") 52 | val code = 53 | """ 54 | @Composable 55 | fun MyComposable() { 56 | val something = movableContentWithReceiverOf { Text("X") } 57 | } 58 | """.trimIndent() 59 | val errors = rule.lint(code) 60 | assertThat(errors) 61 | .hasStartSourceLocations( 62 | SourceLocation(3, 21), 63 | ) 64 | for (error in errors) { 65 | assertThat(error).hasMessage(RememberContentMissing.MovableContentWithReceiverOfNotRemembered) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/UnstableCollectionsCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.detekt 4 | 5 | import io.gitlab.arturbosch.detekt.api.Config 6 | import io.gitlab.arturbosch.detekt.api.SourceLocation 7 | import io.gitlab.arturbosch.detekt.test.assertThat 8 | import io.gitlab.arturbosch.detekt.test.lint 9 | import io.nlopez.compose.rules.UnstableCollections.Companion.createErrorMessage 10 | import org.intellij.lang.annotations.Language 11 | import org.junit.jupiter.api.Test 12 | 13 | class UnstableCollectionsCheckTest { 14 | 15 | private val rule = UnstableCollectionsCheck(Config.empty) 16 | 17 | @Test 18 | fun `errors when a Composable has a List Set Map parameter`() { 19 | @Language("kotlin") 20 | val code = 21 | """ 22 | @Composable 23 | fun Something(a: List) {} 24 | @Composable 25 | fun Something(a: Set) {} 26 | @Composable 27 | fun Something(a: Map) {} 28 | """.trimIndent() 29 | val errors = rule.lint(code) 30 | assertThat(errors) 31 | .hasStartSourceLocations( 32 | SourceLocation(2, 18), 33 | SourceLocation(4, 18), 34 | SourceLocation(6, 18), 35 | ) 36 | assertThat(errors[0]).hasMessage(createErrorMessage("List", "List", "a")) 37 | assertThat(errors[1]).hasMessage(createErrorMessage("Set", "Set", "a")) 38 | assertThat(errors[2]).hasMessage(createErrorMessage("Map", "Map", "a")) 39 | } 40 | 41 | @Test 42 | fun `no errors when a Composable has valid parameters`() { 43 | @Language("kotlin") 44 | val code = 45 | """ 46 | @Composable 47 | fun Something(a: ImmutableList, b: ImmutableSet, c: ImmutableMap) {} 48 | @Composable 49 | fun Something(a: StringList, b: StringSet, c: StringToIntMap) {} 50 | """.trimIndent() 51 | val errors = rule.lint(code) 52 | assertThat(errors).isEmpty() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rules/ktlint/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.shadowJar) 6 | } 7 | 8 | // if publishing and it's not the uber jar, we want to remove the shadowRuntimeElements variant 9 | if (!project.hasProperty("uberJar")) { 10 | val javaComponent = components["java"] as AdhocComponentWithVariants 11 | javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { 12 | skip() 13 | } 14 | } 15 | 16 | tasks.shadowJar { 17 | // Relocate packages that may conflict with the ones IntelliJ IDEA provides as well. 18 | // See https://github.com/nbadal/ktlint-intellij-plugin/blob/main/lib/build.gradle.kts 19 | relocate("org.jetbrains.concurrency", "shadow.org.jetbrains.concurrency") 20 | relocate("org.jetbrains.kotlin.psi.KtPsiFactory", "shadow.org.jetbrains.kotlin.psi.KtPsiFactory") 21 | relocate("org.jetbrains.kotlin.psi.psiUtil", "shadow.org.jetbrains.kotlin.psi.psiUtil") 22 | relocate("org.jetbrains.org", "shadow.org.jetbrains.org") 23 | } 24 | 25 | dependencies { 26 | compileOnlyOrApi(libs.ktlint.rule.engine) 27 | compileOnlyOrApi(libs.ktlint.cli.ruleset.core) 28 | api(projects.rules.common) 29 | 30 | testImplementation(libs.ktlint.test) 31 | testImplementation(libs.junit5) 32 | testImplementation(libs.junit5.params) 33 | testImplementation(libs.assertj) 34 | testImplementation(libs.reflections) 35 | testImplementation(libs.konsist) 36 | } 37 | -------------------------------------------------------------------------------- /rules/ktlint/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=ktlint 2 | POM_NAME=Compose rules for ktlint 3 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/KtlintComposeKtConfig.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules 4 | 5 | import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig 6 | import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty 7 | import io.nlopez.compose.core.ComposeKtConfig 8 | import io.nlopez.compose.core.util.toSnakeCase 9 | 10 | /** 11 | * Manages the configuration for ktlint rules. In ktlint, configs are typically in snake case, while in the 12 | * whole project and in detekt they are camel case, so this class will convert all camel case keys to snake case, 13 | * and add a "compose_" prefix to all of them. 14 | * Results will be memoized as well, as config shouldn't be changing during the lifetime of a rule. 15 | */ 16 | internal class KtlintComposeKtConfig( 17 | private val properties: EditorConfig, 18 | private val editorConfigProperties: Set>, 19 | ) : ComposeKtConfig { 20 | private val cache = mutableMapOf() 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | private fun getValueAsOrPut(key: String, value: () -> T?): T? = cache.getOrPut(key) { value() } as? T 24 | 25 | override fun getInt(key: String, default: Int): Int = getValueAsOrPut(key) { find(key)?.toInt() } ?: default 26 | 27 | override fun getString(key: String, default: String?): String? = getValueAsOrPut(key) { find(key) } ?: default 28 | 29 | override fun getList(key: String, default: List): List = getValueAsOrPut(key) { 30 | find(key)?.split(',', ';')?.map { it.trim() } 31 | } ?: default 32 | 33 | override fun getSet(key: String, default: Set): Set = 34 | getValueAsOrPut(key) { getList(key, default.toList()).toSet() } ?: default 35 | 36 | override fun getBoolean(key: String, default: Boolean): Boolean = getValueAsOrPut(key) { find(key) } ?: default 37 | 38 | private fun find(key: String): T? { 39 | val name = ktlintKey(key) 40 | @Suppress("UNCHECKED_CAST") 41 | return editorConfigProperties.filter { it.name == name }.map { properties[it] }.firstOrNull() as T 42 | } 43 | 44 | private companion object { 45 | private fun ktlintKey(key: String): String = "compose_${key.toSnakeCase()}" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ComposableAnnotationNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.ComposableAnnotationNaming 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class ComposableAnnotationNamingCheck : 10 | KtlintRule("compose:composable-annotation-naming"), 11 | ComposeKtVisitor by ComposableAnnotationNaming() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ComposeRuleSetProvider.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 6 | import com.pinterest.ktlint.rule.engine.core.api.RuleProvider 7 | import com.pinterest.ktlint.rule.engine.core.api.RuleSetId 8 | 9 | class ComposeRuleSetProvider : 10 | RuleSetProviderV3( 11 | customRuleSetId, 12 | ) { 13 | 14 | override fun getRuleProviders(): Set = setOf( 15 | RuleProvider { ComposableAnnotationNamingCheck() }, 16 | RuleProvider { CompositionLocalAllowlistCheck() }, 17 | RuleProvider { CompositionLocalNamingCheck() }, 18 | RuleProvider { ContentEmitterReturningValuesCheck() }, 19 | RuleProvider { ContentSlotReusedCheck() }, 20 | RuleProvider { ContentTrailingLambdaCheck() }, 21 | RuleProvider { DefaultsVisibilityCheck() }, 22 | RuleProvider { LambdaParameterEventTrailingCheck() }, 23 | RuleProvider { LambdaParameterInRestartableEffectCheck() }, 24 | RuleProvider { Material2Check() }, 25 | RuleProvider { ModifierClickableOrderCheck() }, 26 | RuleProvider { ModifierComposedCheck() }, 27 | RuleProvider { ModifierMissingCheck() }, 28 | RuleProvider { ModifierNamingCheck() }, 29 | RuleProvider { ModifierNotUsedAtRootCheck() }, 30 | RuleProvider { ModifierReusedCheck() }, 31 | RuleProvider { ModifierWithoutDefaultCheck() }, 32 | RuleProvider { MultipleContentEmittersCheck() }, 33 | RuleProvider { MutableParametersCheck() }, 34 | RuleProvider { MutableStateAutoboxingCheck() }, 35 | RuleProvider { MutableStateParameterCheck() }, 36 | RuleProvider { NamingCheck() }, 37 | RuleProvider { ParameterNamingCheck() }, 38 | RuleProvider { ParameterOrderCheck() }, 39 | RuleProvider { PreviewAnnotationNamingCheck() }, 40 | RuleProvider { PreviewNamingCheck() }, 41 | RuleProvider { PreviewPublicCheck() }, 42 | RuleProvider { RememberContentMissingCheck() }, 43 | RuleProvider { RememberStateMissingCheck() }, 44 | RuleProvider { UnstableCollectionsCheck() }, 45 | RuleProvider { ViewModelForwardingCheck() }, 46 | RuleProvider { ViewModelInjectionCheck() }, 47 | ) 48 | 49 | private companion object { 50 | val customRuleSetId = RuleSetId("compose") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/CompositionLocalAllowlistCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.CompositionLocalAllowlist 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class CompositionLocalAllowlistCheck : 10 | KtlintRule( 11 | id = "compose:compositionlocal-allowlist", 12 | editorConfigProperties = setOf(compositionLocalAllowlistProperty), 13 | ), 14 | ComposeKtVisitor by CompositionLocalAllowlist() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/CompositionLocalNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.CompositionLocalNaming 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class CompositionLocalNamingCheck : 10 | KtlintRule("compose:compositionlocal-naming"), 11 | ComposeKtVisitor by CompositionLocalNaming() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ContentEmitterReturningValuesCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.ContentEmitterReturningValues 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class ContentEmitterReturningValuesCheck : 10 | KtlintRule( 11 | id = "compose:content-emitter-returning-values-check", 12 | editorConfigProperties = setOf(contentEmittersProperty), 13 | ), 14 | ComposeKtVisitor by ContentEmitterReturningValues() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ContentSlotReusedCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.ContentSlotReused 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class ContentSlotReusedCheck : 10 | KtlintRule( 11 | id = "compose:content-slot-reused", 12 | editorConfigProperties = setOf(treatAsLambda, treatAsComposableLambda), 13 | ), 14 | ComposeKtVisitor by ContentSlotReused() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ContentTrailingLambdaCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.ContentTrailingLambda 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class ContentTrailingLambdaCheck : 10 | KtlintRule( 11 | id = "compose:content-trailing-lambda", 12 | editorConfigProperties = setOf(treatAsLambda, treatAsComposableLambda), 13 | ), 14 | ComposeKtVisitor by ContentTrailingLambda() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/DefaultsVisibilityCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.DefaultsVisibility 7 | import io.nlopez.compose.rules.KtlintRule 8 | 9 | class DefaultsVisibilityCheck : 10 | KtlintRule("compose:defaults-visibility"), 11 | ComposeKtVisitor by DefaultsVisibility() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/LambdaParameterEventTrailingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.LambdaParameterEventTrailing 8 | 9 | class LambdaParameterEventTrailingCheck : 10 | KtlintRule( 11 | id = "compose:lambda-param-event-trailing", 12 | editorConfigProperties = setOf(contentEmittersProperty, contentEmittersDenylist), 13 | ), 14 | ComposeKtVisitor by LambdaParameterEventTrailing() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/LambdaParameterInRestartableEffectCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.LambdaParameterInRestartableEffect 8 | 9 | class LambdaParameterInRestartableEffectCheck : 10 | KtlintRule( 11 | id = "compose:lambda-param-in-effect", 12 | editorConfigProperties = setOf(treatAsLambda), 13 | ), 14 | ComposeKtVisitor by LambdaParameterInRestartableEffect() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/Material2Check.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.rules.KtlintRule 9 | import io.nlopez.compose.rules.Material2 10 | import org.jetbrains.kotlin.psi.KtFile 11 | 12 | class Material2Check : 13 | KtlintRule( 14 | id = "compose:material-two", 15 | editorConfigProperties = setOf(allowedFromM2, disallowMaterial2), 16 | ), 17 | ComposeKtVisitor { 18 | private val visitor = Material2() 19 | 20 | override fun visitFile(file: KtFile, emitter: Emitter, config: ComposeKtConfig) { 21 | // ktlint allows all rules by default, so we'll add an extra param to make sure it's disabled by default 22 | if (config.getBoolean("disallowMaterial2", false)) { 23 | visitor.visitFile(file, emitter, config) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierClickableOrderCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierClickableOrder 8 | 9 | class ModifierClickableOrderCheck : 10 | KtlintRule( 11 | id = "compose:modifier-clickable-order", 12 | editorConfigProperties = setOf(customModifiers), 13 | ), 14 | ComposeKtVisitor by ModifierClickableOrder() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierComposedCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierComposed 8 | 9 | class ModifierComposedCheck : 10 | KtlintRule( 11 | id = "compose:modifier-composed-check", 12 | editorConfigProperties = setOf(customModifiers), 13 | ), 14 | ComposeKtVisitor by ModifierComposed() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierMissingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierMissing 8 | 9 | class ModifierMissingCheck : 10 | KtlintRule( 11 | id = "compose:modifier-missing-check", 12 | editorConfigProperties = setOf( 13 | checkModifiersForVisibility, 14 | contentEmittersProperty, 15 | customModifiers, 16 | contentEmittersDenylist, 17 | modifierMissingIgnoreAnnotated, 18 | ), 19 | ), 20 | ComposeKtVisitor by ModifierMissing() 21 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierNaming 8 | 9 | class ModifierNamingCheck : 10 | KtlintRule( 11 | id = "compose:modifier-naming", 12 | editorConfigProperties = setOf(customModifiers), 13 | ), 14 | ComposeKtVisitor by ModifierNaming() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierNotUsedAtRootCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierNotUsedAtRoot 8 | 9 | class ModifierNotUsedAtRootCheck : 10 | KtlintRule( 11 | id = "compose:modifier-not-used-at-root", 12 | editorConfigProperties = setOf(contentEmittersProperty, customModifiers, contentEmittersDenylist), 13 | ), 14 | ComposeKtVisitor by ModifierNotUsedAtRoot() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierReusedCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierReused 8 | 9 | class ModifierReusedCheck : 10 | KtlintRule( 11 | id = "compose:modifier-reused-check", 12 | editorConfigProperties = setOf(contentEmittersProperty, customModifiers, contentEmittersDenylist), 13 | ), 14 | ComposeKtVisitor by ModifierReused() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ModifierWithoutDefaultCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ModifierWithoutDefault 8 | 9 | class ModifierWithoutDefaultCheck : 10 | KtlintRule( 11 | id = "compose:modifier-without-default-check", 12 | editorConfigProperties = setOf(customModifiers), 13 | ), 14 | ComposeKtVisitor by ModifierWithoutDefault() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/MultipleContentEmittersCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.MultipleContentEmitters 8 | 9 | class MultipleContentEmittersCheck : 10 | KtlintRule( 11 | id = "compose:multiple-emitters-check", 12 | editorConfigProperties = setOf(contentEmittersProperty, contentEmittersDenylist), 13 | ), 14 | ComposeKtVisitor by MultipleContentEmitters() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/MutableParametersCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.MutableParameters 8 | 9 | class MutableParametersCheck : 10 | KtlintRule("compose:mutable-params-check"), 11 | ComposeKtVisitor by MutableParameters() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/MutableStateAutoboxingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.MutableStateAutoboxing 8 | 9 | class MutableStateAutoboxingCheck : 10 | KtlintRule("compose:mutable-state-autoboxing"), 11 | ComposeKtVisitor by MutableStateAutoboxing() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/MutableStateParameterCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.MutableStateParameter 8 | 9 | class MutableStateParameterCheck : 10 | KtlintRule("compose:mutable-state-param-check"), 11 | ComposeKtVisitor by MutableStateParameter() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/NamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.Naming 8 | 9 | class NamingCheck : 10 | KtlintRule( 11 | id = "compose:naming-check", 12 | editorConfigProperties = setOf(allowedComposeNamingNames), 13 | ), 14 | ComposeKtVisitor by Naming() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ParameterNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ParameterNaming 8 | 9 | class ParameterNamingCheck : 10 | KtlintRule( 11 | id = "compose:parameter-naming", 12 | editorConfigProperties = setOf(treatAsLambda, allowedLambdaParameterNames), 13 | ), 14 | ComposeKtVisitor by ParameterNaming() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ParameterOrderCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ParameterOrder 8 | 9 | class ParameterOrderCheck : 10 | KtlintRule( 11 | id = "compose:param-order-check", 12 | editorConfigProperties = setOf(treatAsLambda), 13 | ), 14 | ComposeKtVisitor by ParameterOrder() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/PreviewAnnotationNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.PreviewAnnotationNaming 8 | 9 | class PreviewAnnotationNamingCheck : 10 | KtlintRule("compose:preview-annotation-naming"), 11 | ComposeKtVisitor by PreviewAnnotationNaming() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/PreviewNamingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.rules.KtlintRule 9 | import io.nlopez.compose.rules.PreviewNaming 10 | import org.jetbrains.kotlin.psi.KtFunction 11 | 12 | class PreviewNamingCheck : 13 | KtlintRule( 14 | id = "compose:preview-naming", 15 | editorConfigProperties = setOf(composePreviewNamingEnabled, composePreviewNamingStrategy), 16 | ), 17 | ComposeKtVisitor { 18 | private val visitor = PreviewNaming() 19 | 20 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 21 | // ktlint allows all rules by default, so we'll add an extra param to make sure it's disabled by default 22 | if (config.getBoolean("previewNamingEnabled", false)) { 23 | visitor.visitComposable(function, emitter, config) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/PreviewPublicCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.PreviewPublic 8 | 9 | class PreviewPublicCheck : 10 | KtlintRule("compose:preview-public-check"), 11 | ComposeKtVisitor by PreviewPublic() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/RememberContentMissingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.RememberContentMissing 8 | 9 | class RememberContentMissingCheck : 10 | KtlintRule("compose:remember-content-missing-check"), 11 | ComposeKtVisitor by RememberContentMissing() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/RememberStateMissingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.RememberStateMissing 8 | 9 | class RememberStateMissingCheck : 10 | KtlintRule("compose:remember-missing-check"), 11 | ComposeKtVisitor by RememberStateMissing() 12 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/UnstableCollectionsCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtConfig 6 | import io.nlopez.compose.core.ComposeKtVisitor 7 | import io.nlopez.compose.core.Emitter 8 | import io.nlopez.compose.rules.KtlintRule 9 | import io.nlopez.compose.rules.UnstableCollections 10 | import org.jetbrains.kotlin.psi.KtFunction 11 | 12 | class UnstableCollectionsCheck : 13 | KtlintRule( 14 | id = "compose:unstable-collections", 15 | editorConfigProperties = setOf(disallowUnstableCollections), 16 | ), 17 | ComposeKtVisitor { 18 | 19 | private val visitor = UnstableCollections() 20 | 21 | override fun visitComposable(function: KtFunction, emitter: Emitter, config: ComposeKtConfig) { 22 | // ktlint allows all rules by default, so we'll add an extra param to make sure it's disabled by default 23 | if (config.getBoolean("disallowUnstableCollections", false)) { 24 | visitor.visitComposable(function, emitter, config) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ViewModelForwardingCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ViewModelForwarding 8 | 9 | class ViewModelForwardingCheck : 10 | KtlintRule( 11 | id = "compose:vm-forwarding-check", 12 | editorConfigProperties = setOf(allowedStateHolderNames, allowedForwarding, allowedForwardingOfTypes), 13 | ), 14 | ComposeKtVisitor by ViewModelForwarding() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/ViewModelInjectionCheck.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import io.nlopez.compose.core.ComposeKtVisitor 6 | import io.nlopez.compose.rules.KtlintRule 7 | import io.nlopez.compose.rules.ViewModelInjection 8 | 9 | class ViewModelInjectionCheck : 10 | KtlintRule( 11 | id = "compose:vm-injection-check", 12 | editorConfigProperties = setOf(viewModelFactories), 13 | ), 14 | ComposeKtVisitor by ViewModelInjection() 15 | -------------------------------------------------------------------------------- /rules/ktlint/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3: -------------------------------------------------------------------------------- 1 | io.nlopez.compose.rules.ktlint.ComposeRuleSetProvider 2 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/ComposableAnnotationNamingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.ComposableAnnotationNaming 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class ComposableAnnotationNamingCheckTest { 12 | 13 | private val ruleAssertThat = assertThatRule { ComposableAnnotationNamingCheck() } 14 | 15 | @Test 16 | fun `passes for non-composable annotations`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | annotation class Banana 21 | """.trimIndent() 22 | ruleAssertThat(code).hasNoLintViolations() 23 | } 24 | 25 | @Test 26 | fun `passes for composable annotations with the proper names`() { 27 | @Language("kotlin") 28 | val code = 29 | """ 30 | @ComposableTargetMarker 31 | annotation class BananaComposable 32 | @ComposableTargetMarker 33 | annotation class AppleComposable 34 | """.trimIndent() 35 | ruleAssertThat(code).hasNoLintViolations() 36 | } 37 | 38 | @Test 39 | fun `errors when a composable annotation is not correctly named`() { 40 | @Language("kotlin") 41 | val code = 42 | """ 43 | @ComposableTargetMarker 44 | annotation class Banana 45 | @ComposableTargetMarker 46 | annotation class Apple 47 | """.trimIndent() 48 | ruleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 49 | LintViolation( 50 | line = 2, 51 | col = 18, 52 | detail = ComposableAnnotationNaming.ComposableAnnotationDoesNotEndWithComposable, 53 | ), 54 | LintViolation( 55 | line = 4, 56 | col = 18, 57 | detail = ComposableAnnotationNaming.ComposableAnnotationDoesNotEndWithComposable, 58 | ), 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/ComposeRuleSetProviderTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.lemonappdev.konsist.api.Konsist 6 | import com.lemonappdev.konsist.api.ext.list.withAllParentsOf 7 | import com.lemonappdev.konsist.api.verify.assertTrue 8 | import io.nlopez.compose.core.ComposeKtVisitor 9 | import io.nlopez.compose.rules.KtlintRule 10 | import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat 11 | import org.junit.jupiter.api.Test 12 | import org.reflections.Reflections 13 | import org.reflections.scanners.Scanners 14 | import org.reflections.util.ConfigurationBuilder 15 | 16 | class ComposeRuleSetProviderTest { 17 | 18 | private val ruleSetProvider = ComposeRuleSetProvider() 19 | private val ruleClassesInPackage = Reflections(ruleSetProvider.javaClass.packageName) 20 | .getSubTypesOf(KtlintRule::class.java) 21 | 22 | @Test 23 | fun `ensure all rules in the package are represented in the ruleset`() { 24 | val ruleSet = ruleSetProvider.getRuleProviders() 25 | val ruleClassesInRuleSet = ruleSet.map { it.createNewRuleInstance() } 26 | .filterIsInstance() 27 | .map { it::class.java } 28 | .toSet() 29 | assertThat(ruleClassesInRuleSet).containsExactlyInAnyOrderElementsOf(ruleClassesInPackage) 30 | } 31 | 32 | @Test 33 | fun `ensure all rules in the package are listed in alphabetical order`() { 34 | val isOrdered = ruleSetProvider.getRuleProviders() 35 | .filterIsInstance() 36 | .asSequence() 37 | .map { it::class.java.simpleName } 38 | .zipWithNext { a, b -> a <= b } 39 | .all { it } 40 | assertThat(isOrdered) 41 | .describedAs("ComposeRuleSetProvider should have the rules in alphabetical order") 42 | .isTrue() 43 | } 44 | 45 | @Test 46 | fun `ensure all available rules have a ktlint rule`() { 47 | val ktlintRuleNames = ruleClassesInPackage.map { it.simpleName } 48 | 49 | val commonRulesReflections = Reflections( 50 | ConfigurationBuilder() 51 | .setClassLoaders(arrayOf(ComposeKtVisitor::class.java.classLoader)) 52 | .setScanners(Scanners.SubTypes), 53 | ) 54 | val ruleNames = commonRulesReflections.getSubTypesOf(ComposeKtVisitor::class.java).map { it.simpleName } 55 | 56 | for (ruleName in ruleNames) { 57 | assertThat(ktlintRuleNames) 58 | .describedAs { "$ruleName should have a ktlint rule named ${ruleName}Check" } 59 | .contains("${ruleName}Check") 60 | } 61 | } 62 | 63 | @Test 64 | fun `ensure all ktlint rules have a unit test`() { 65 | Konsist.scopeFromProduction() 66 | .classes() 67 | .withAllParentsOf(KtlintRule::class) 68 | .assertTrue { clazz -> 69 | clazz.testClasses { it.hasNameContaining(clazz.name) }.isNotEmpty() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/CompositionLocalAllowlistCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.CompositionLocalAllowlist 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class CompositionLocalAllowlistCheckTest { 12 | 13 | private val allowlistRuleAssertThat = assertThatRule { CompositionLocalAllowlistCheck() } 14 | 15 | @Test 16 | fun `error when a CompositionLocal is defined`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | private val LocalApple = staticCompositionLocalOf { "Apple" } 21 | internal val LocalPlum: String = staticCompositionLocalOf { "Plum" } 22 | val LocalPrune = compositionLocalOf { "Prune" } 23 | private val LocalKiwi: String = compositionLocalOf { "Kiwi" } 24 | """.trimIndent() 25 | allowlistRuleAssertThat(code) 26 | .hasLintViolationsWithoutAutoCorrect( 27 | LintViolation( 28 | line = 1, 29 | col = 13, 30 | detail = CompositionLocalAllowlist.CompositionLocalNotInAllowlist, 31 | ), 32 | LintViolation( 33 | line = 2, 34 | col = 14, 35 | detail = CompositionLocalAllowlist.CompositionLocalNotInAllowlist, 36 | ), 37 | LintViolation( 38 | line = 3, 39 | col = 5, 40 | detail = CompositionLocalAllowlist.CompositionLocalNotInAllowlist, 41 | ), 42 | LintViolation( 43 | line = 4, 44 | col = 13, 45 | detail = CompositionLocalAllowlist.CompositionLocalNotInAllowlist, 46 | ), 47 | ) 48 | } 49 | 50 | @Test 51 | fun `passes when a CompositionLocal is defined but it's in the allowlist`() { 52 | @Language("kotlin") 53 | val code = 54 | """ 55 | val LocalBanana = staticCompositionLocalOf { "Banana" } 56 | val LocalPotato = compositionLocalOf { "Potato" } 57 | """.trimIndent() 58 | allowlistRuleAssertThat(code) 59 | .withEditorConfigOverride( 60 | compositionLocalAllowlistProperty to "LocalPotato,LocalBanana", 61 | ) 62 | .hasNoLintViolations() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/CompositionLocalNamingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.CompositionLocalNaming 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class CompositionLocalNamingCheckTest { 12 | 13 | private val ruleAssertThat = assertThatRule { CompositionLocalNamingCheck() } 14 | 15 | @Test 16 | fun `error when a CompositionLocal has a wrong name`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | val AppleLocal = staticCompositionLocalOf { "Apple" } 21 | val Plum: String = staticCompositionLocalOf { "Plum" } 22 | """.trimIndent() 23 | ruleAssertThat(code) 24 | .hasLintViolationsWithoutAutoCorrect( 25 | LintViolation( 26 | line = 1, 27 | col = 5, 28 | detail = CompositionLocalNaming.CompositionLocalNeedsLocalPrefix, 29 | ), 30 | LintViolation( 31 | line = 2, 32 | col = 5, 33 | detail = CompositionLocalNaming.CompositionLocalNeedsLocalPrefix, 34 | ), 35 | ) 36 | } 37 | 38 | @Test 39 | fun `passes when a CompositionLocal is well named`() { 40 | @Language("kotlin") 41 | val code = 42 | """ 43 | val LocalBanana = staticCompositionLocalOf { "Banana" } 44 | val LocalPotato = compositionLocalOf { "Potato" } 45 | """.trimIndent() 46 | ruleAssertThat(code).hasNoLintViolations() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/ContentEmitterReturningValuesCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.ContentEmitterReturningValues 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class ContentEmitterReturningValuesCheckTest { 12 | 13 | private val emittersRuleAssertThat = assertThatRule { ContentEmitterReturningValuesCheck() } 14 | 15 | @Test 16 | fun `error out when detecting a content emitting composable that returns something other than unit`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | @Composable 21 | fun Something(): String { // This one emits content directly and should fail 22 | Text("Hi") 23 | return "Potato" 24 | } 25 | @Composable 26 | fun Something2(): WhateverState { // This one emits content indirectly and should fail too 27 | Something3() 28 | return remember { WhateverState() } 29 | } 30 | @Composable 31 | fun Something3() { // This one is fine but calling it should make Something2 fail 32 | Potato(icon = HorizonIcon.Arrow) 33 | } 34 | @Composable 35 | fun Something4(): String { // This one is using a composable defined in the config 36 | Banana() 37 | } 38 | """.trimIndent() 39 | emittersRuleAssertThat(code) 40 | .withEditorConfigOverride( 41 | contentEmittersProperty to "Potato,Banana", 42 | ) 43 | .hasLintViolationsWithoutAutoCorrect( 44 | LintViolation( 45 | line = 2, 46 | col = 5, 47 | detail = ContentEmitterReturningValues.ContentEmitterReturningValuesToo, 48 | ), 49 | LintViolation( 50 | line = 7, 51 | col = 5, 52 | detail = ContentEmitterReturningValues.ContentEmitterReturningValuesToo, 53 | ), 54 | LintViolation( 55 | line = 16, 56 | col = 5, 57 | detail = ContentEmitterReturningValues.ContentEmitterReturningValuesToo, 58 | ), 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/DefaultsVisibilityCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.DefaultsVisibility.Companion.createMessage 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class DefaultsVisibilityCheckTest { 12 | 13 | private val modifierRuleAssertThat = assertThatRule { DefaultsVisibilityCheck() } 14 | 15 | @Test 16 | fun `errors when a defaults object has less visibility than the composable that uses it`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | internal object MyComposableDefaults 21 | @Composable 22 | fun MyComposable(someParam: Bleh = MyComposableDefaults.someParam) { } 23 | private object MyOtherComposableDefaults 24 | @Composable 25 | internal fun MyOtherComposable() { 26 | val someUsage = MyOtherComposableDefaults.someParam.someMethod() 27 | } 28 | """.trimIndent() 29 | modifierRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 30 | LintViolation( 31 | line = 1, 32 | col = 17, 33 | detail = createMessage("public", "MyComposableDefaults", "internal"), 34 | ), 35 | LintViolation( 36 | line = 4, 37 | col = 16, 38 | detail = createMessage("internal", "MyOtherComposableDefaults", "private"), 39 | ), 40 | ) 41 | } 42 | 43 | @Test 44 | fun `passes when a defaults object has the same visibility as any of the overloaded composables that match it`() { 45 | @Language("kotlin") 46 | val code = 47 | """ 48 | object MyComposableDefaults 49 | @Composable 50 | fun MyComposable(someParam: Bleh = MyComposableDefaults.someParam) { } 51 | internal object MyOtherComposableDefaults 52 | @Composable 53 | internal fun MyOtherComposable() { 54 | val someUsage = MyOtherComposableDefaults.someParam.someMethod() 55 | } 56 | object MyThirdComposableDefaults 57 | @Composable 58 | fun MyThirdComposable(a: A) { 59 | val someUsage = MyThirdComposableDefaults.someParam 60 | } 61 | @Composable 62 | internal fun MyThirdComposable(b: B) { 63 | val someUsage = MyThirdComposableDefaults.someParam 64 | } 65 | """.trimIndent() 66 | modifierRuleAssertThat(code).hasNoLintViolations() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/ModifierComposedCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.ModifierComposed 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class ModifierComposedCheckTest { 12 | 13 | private val modifierRuleAssertThat = assertThatRule { ModifierComposedCheck() } 14 | 15 | @Test 16 | fun `errors when a composable Modifier extension is detected`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | fun Modifier.something1(): Modifier = composed {} 21 | fun Modifier.something2() = composed {} 22 | fun Modifier.something3() { 23 | return composed {} 24 | } 25 | """.trimIndent() 26 | 27 | modifierRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 28 | LintViolation( 29 | line = 1, 30 | col = 14, 31 | detail = ModifierComposed.ComposedModifier, 32 | ), 33 | LintViolation( 34 | line = 2, 35 | col = 14, 36 | detail = ModifierComposed.ComposedModifier, 37 | ), 38 | LintViolation( 39 | line = 3, 40 | col = 14, 41 | detail = ModifierComposed.ComposedModifier, 42 | ), 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/ModifierNamingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.ModifierNaming 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class ModifierNamingCheckTest { 12 | 13 | private val modifierRuleAssertThat = assertThatRule { ModifierNamingCheck() } 14 | 15 | @Test 16 | fun `errors when a Composable has a modifier not named modifier`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | @Composable 21 | fun Something1(m: Modifier) {} 22 | @Composable 23 | fun Something2(m: Modifier, m2: Modifier) {} 24 | """.trimIndent() 25 | 26 | modifierRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 27 | LintViolation( 28 | line = 2, 29 | col = 16, 30 | detail = ModifierNaming.ModifiersAreSupposedToBeCalledModifierWhenAlone, 31 | ), 32 | LintViolation( 33 | line = 4, 34 | col = 16, 35 | detail = ModifierNaming.ModifiersAreSupposedToEndInModifierWhenMultiple, 36 | ), 37 | LintViolation( 38 | line = 4, 39 | col = 29, 40 | detail = ModifierNaming.ModifiersAreSupposedToEndInModifierWhenMultiple, 41 | ), 42 | ) 43 | } 44 | 45 | @Test 46 | fun `passes when the modifiers are named correctly`() { 47 | @Language("kotlin") 48 | val code = 49 | """ 50 | @Composable 51 | fun Something1(modifier: Modifier) {} 52 | @Composable 53 | fun Something2(modifier: Modifier, otherModifier: Modifier) {} 54 | """.trimIndent() 55 | 56 | modifierRuleAssertThat(code).hasNoLintViolations() 57 | } 58 | 59 | @Test 60 | fun `errors when a Composable has a single modifier not named modifier but ends with modifier`() { 61 | @Language("kotlin") 62 | val code = 63 | """ 64 | @Composable 65 | fun Something1(myModifier: Modifier) {} 66 | """.trimIndent() 67 | 68 | modifierRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( 69 | line = 2, 70 | col = 16, 71 | detail = ModifierNaming.ModifiersAreSupposedToBeCalledModifierWhenAlone, 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/MutableParametersCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.MutableParameters 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class MutableParametersCheckTest { 12 | 13 | private val mutableParamRuleAssertThat = assertThatRule { MutableParametersCheck() } 14 | 15 | @Test 16 | fun `errors when a Composable has a mutable parameter`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | @Composable 21 | fun Something(a: ArrayList) {} 22 | @Composable 23 | fun Something(a: HashSet) {} 24 | @Composable 25 | fun Something(a: MutableMap) {} 26 | """.trimIndent() 27 | mutableParamRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 28 | LintViolation( 29 | line = 2, 30 | col = 15, 31 | detail = MutableParameters.MutableParameterInCompose, 32 | ), 33 | LintViolation( 34 | line = 4, 35 | col = 15, 36 | detail = MutableParameters.MutableParameterInCompose, 37 | ), 38 | LintViolation( 39 | line = 6, 40 | col = 15, 41 | detail = MutableParameters.MutableParameterInCompose, 42 | ), 43 | ) 44 | } 45 | 46 | @Test 47 | fun `no errors when a Composable has valid parameters`() { 48 | @Language("kotlin") 49 | val code = 50 | """ 51 | @Composable 52 | fun Something(a: String, b: (Int) -> Unit) {} 53 | @Composable 54 | fun Something(a: State) {} 55 | """.trimIndent() 56 | mutableParamRuleAssertThat(code).hasNoLintViolations() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/MutableStateParameterCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.MutableStateParameter 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class MutableStateParameterCheckTest { 12 | 13 | private val mutableParamRuleAssertThat = assertThatRule { MutableStateParameterCheck() } 14 | 15 | @Test 16 | fun `errors when a Composable has a mutable parameter`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | @Composable 21 | fun Something(a: MutableState) {} 22 | """.trimIndent() 23 | mutableParamRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 24 | LintViolation( 25 | line = 2, 26 | col = 15, 27 | detail = MutableStateParameter.MutableStateParameterInCompose, 28 | ), 29 | ) 30 | } 31 | 32 | @Test 33 | fun `no errors when a Composable has valid parameters`() { 34 | @Language("kotlin") 35 | val code = 36 | """ 37 | @Composable 38 | fun Something(a: String, b: (Int) -> Unit) {} 39 | @Composable 40 | fun Something(a: State) {} 41 | """.trimIndent() 42 | mutableParamRuleAssertThat(code).hasNoLintViolations() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/PreviewPublicCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.PreviewPublic 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class PreviewPublicCheckTest { 12 | 13 | private val ruleAssertThat = assertThatRule { PreviewPublicCheck() } 14 | 15 | @Test 16 | fun `passes for non-preview public composables`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | @Composable 21 | fun MyComposable() { } 22 | """.trimIndent() 23 | ruleAssertThat(code).hasNoLintViolations() 24 | } 25 | 26 | @Test 27 | fun `errors for preview public composables`() { 28 | @Language("kotlin") 29 | val code = 30 | """ 31 | @Preview 32 | @Composable 33 | fun MyComposable() { } 34 | @CombinedPreviews 35 | @Composable 36 | fun MyComposable() { } 37 | """.trimIndent() 38 | ruleAssertThat(code).hasLintViolations( 39 | LintViolation( 40 | line = 3, 41 | col = 5, 42 | detail = PreviewPublic.ComposablesPreviewShouldNotBePublic, 43 | ), 44 | LintViolation( 45 | line = 6, 46 | col = 5, 47 | detail = PreviewPublic.ComposablesPreviewShouldNotBePublic, 48 | ), 49 | ) 50 | } 51 | 52 | @Test 53 | fun `passes when a non-public preview composable uses preview params`() { 54 | @Language("kotlin") 55 | val code = 56 | """ 57 | @Preview 58 | @Composable 59 | private fun MyComposable(user: User) { 60 | } 61 | @CombinedPreviews 62 | @Composable 63 | internal fun MyComposable(user: User) { 64 | } 65 | """.trimIndent() 66 | ruleAssertThat(code).hasNoLintViolations() 67 | } 68 | 69 | @Test 70 | fun `autofix makes private the public preview`() { 71 | @Language("kotlin") 72 | val badCode = """ 73 | @Preview 74 | @Composable 75 | fun MyComposable(user: User) { 76 | } 77 | @CombinedPreviews 78 | @Composable 79 | fun MyComposable(user: User) { 80 | } 81 | """.trimIndent() 82 | 83 | @Language("kotlin") 84 | val expectedCode = """ 85 | @Preview 86 | @Composable 87 | private fun MyComposable(user: User) { 88 | } 89 | @CombinedPreviews 90 | @Composable 91 | private fun MyComposable(user: User) { 92 | } 93 | """.trimIndent() 94 | ruleAssertThat(badCode).isFormattedAs(expectedCode) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/RememberContentMissingCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.RememberContentMissing 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class RememberContentMissingCheckTest { 12 | 13 | private val rememberRuleAssertThat = assertThatRule { RememberContentMissingCheck() } 14 | 15 | @Test 16 | fun `passes when a non-remembered movableContentOf is used outside of a Composable`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | val msof = movableContentOf { Text("X") } 21 | """.trimIndent() 22 | rememberRuleAssertThat(code).hasNoLintViolations() 23 | } 24 | 25 | @Test 26 | fun `errors when a non-remembered movableContentOf is used in a Composable`() { 27 | @Language("kotlin") 28 | val code = 29 | """ 30 | @Composable 31 | fun MyComposable() { 32 | val something = movableContentOf { Text("X") } 33 | } 34 | """.trimIndent() 35 | rememberRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 36 | LintViolation( 37 | line = 3, 38 | col = 21, 39 | detail = RememberContentMissing.MovableContentOfNotRemembered, 40 | ), 41 | ) 42 | } 43 | 44 | @Test 45 | fun `errors when a non-remembered movableContentWithReceiverOf is used in a Composable`() { 46 | @Language("kotlin") 47 | val code = 48 | """ 49 | @Composable 50 | fun MyComposable() { 51 | val something = movableContentWithReceiverOf { Text("X") } 52 | } 53 | """.trimIndent() 54 | rememberRuleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 55 | LintViolation( 56 | line = 3, 57 | col = 21, 58 | detail = RememberContentMissing.MovableContentWithReceiverOfNotRemembered, 59 | ), 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/UnstableCollectionsCheckTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | package io.nlopez.compose.rules.ktlint 4 | 5 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 6 | import com.pinterest.ktlint.test.LintViolation 7 | import io.nlopez.compose.rules.UnstableCollections.Companion.createErrorMessage 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class UnstableCollectionsCheckTest { 12 | 13 | private val ruleAssertThat = assertThatRule { UnstableCollectionsCheck() } 14 | 15 | @Test 16 | fun `errors when a Composable has a List Set Map parameter`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | @Composable 21 | fun Something(a: List) {} 22 | @Composable 23 | fun Something(a: Set) {} 24 | @Composable 25 | fun Something(a: Map) {} 26 | """.trimIndent() 27 | ruleAssertThat(code) 28 | .withEditorConfigOverride(disallowUnstableCollections to true) 29 | .hasLintViolationsWithoutAutoCorrect( 30 | LintViolation( 31 | line = 2, 32 | col = 18, 33 | detail = createErrorMessage("List", "List", "a"), 34 | ), 35 | LintViolation( 36 | line = 4, 37 | col = 18, 38 | detail = createErrorMessage("Set", "Set", "a"), 39 | ), 40 | LintViolation( 41 | line = 6, 42 | col = 18, 43 | detail = createErrorMessage("Map", "Map", "a"), 44 | ), 45 | ) 46 | } 47 | 48 | @Test 49 | fun `passes even if there are errors if disallowUnstableCollections is false`() { 50 | @Language("kotlin") 51 | val code = 52 | """ 53 | @Composable 54 | fun Something(a: List) {} 55 | @Composable 56 | fun Something(a: Set) {} 57 | @Composable 58 | fun Something(a: Map) {} 59 | """.trimIndent() 60 | ruleAssertThat(code).hasNoLintViolations() 61 | } 62 | 63 | @Test 64 | fun `no errors when a Composable has valid parameters`() { 65 | @Language("kotlin") 66 | val code = 67 | """ 68 | @Composable 69 | fun Something(a: ImmutableList, b: ImmutableSet, c: ImmutableMap) {} 70 | @Composable 71 | fun Something(a: StringList, b: StringSet, c: StringToIntMap) {} 72 | """.trimIndent() 73 | ruleAssertThat(code) 74 | .withEditorConfigOverride(disallowUnstableCollections to true) 75 | .hasNoLintViolations() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scripts/templates/DetektRule.kt.template: -------------------------------------------------------------------------------- 1 | package io.nlopez.compose.rules.detekt 2 | 3 | import io.gitlab.arturbosch.detekt.api.Config 4 | import io.gitlab.arturbosch.detekt.api.Debt 5 | import io.gitlab.arturbosch.detekt.api.Issue 6 | import io.gitlab.arturbosch.detekt.api.Severity 7 | import io.nlopez.compose.core.ComposeKtVisitor 8 | import io.nlopez.compose.rules.${ruleName} 9 | import io.nlopez.compose.rules.DetektRule 10 | 11 | class ${detektRuleName}(config: Config) : 12 | DetektRule(config), 13 | ComposeKtVisitor by ${ruleName}() { 14 | 15 | override val issue: Issue = Issue( 16 | id = "${ruleName}", 17 | severity = Severity.CodeSmell, 18 | description = ${ruleName}.${ruleName}ErrorMessage, 19 | debt = Debt.FIVE_MINS, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /scripts/templates/DetektRuleTest.kt.template: -------------------------------------------------------------------------------- 1 | package io.nlopez.compose.rules.detekt 2 | 3 | import io.gitlab.arturbosch.detekt.api.Config 4 | import io.gitlab.arturbosch.detekt.api.SourceLocation 5 | import io.gitlab.arturbosch.detekt.test.assertThat 6 | import io.gitlab.arturbosch.detekt.test.lint 7 | import io.nlopez.compose.rules.${ruleName} 8 | import org.intellij.lang.annotations.Language 9 | import org.junit.jupiter.api.Test 10 | 11 | class ${detektRuleName}Test { 12 | 13 | private val rule = ${detektRuleName}(Config.empty) 14 | 15 | @Test 16 | fun `errors for X case`() { 17 | @Language("kotlin") 18 | val code = 19 | """ 20 | TODO() 21 | """.trimIndent() 22 | val errors = rule.lint(code) 23 | assertThat(errors).hasStartSourceLocations( 24 | SourceLocation(2, 5), 25 | ) 26 | for (error in errors) { 27 | assertThat(error) 28 | .hasMessage(${ruleName}.${ruleName}ErrorMessage) 29 | } 30 | } 31 | 32 | @Test 33 | fun `passes for X case`() { 34 | @Language("kotlin") 35 | val code = 36 | """ 37 | TODO() 38 | """.trimIndent() 39 | val errors = rule.lint(code) 40 | assertThat(errors).isEmpty() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scripts/templates/KtlintRule.kt.template: -------------------------------------------------------------------------------- 1 | package io.nlopez.compose.rules.ktlint 2 | 3 | import io.nlopez.compose.core.ComposeKtVisitor 4 | import io.nlopez.compose.rules.${ruleName} 5 | import io.nlopez.compose.rules.KtlintRule 6 | 7 | class ${ruleName} : 8 | KtlintRule("compose:${ktlintRuleId}"), 9 | ComposeKtVisitor by ${ruleName}() 10 | -------------------------------------------------------------------------------- /scripts/templates/KtlintRuleTest.kt.template: -------------------------------------------------------------------------------- 1 | package io.nlopez.compose.rules.ktlint 2 | 3 | import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule 4 | import com.pinterest.ktlint.test.LintViolation 5 | import io.nlopez.compose.rules.${ruleName} 6 | import org.intellij.lang.annotations.Language 7 | import org.junit.jupiter.api.Test 8 | 9 | class ${ktlintRuleName}Test { 10 | 11 | private val ruleAssertThat = assertThatRule { ${ktlintRuleName}() } 12 | 13 | @Test 14 | fun `errors for X case`() { 15 | @Language("kotlin") 16 | val code = 17 | """ 18 | TODO() 19 | """.trimIndent() 20 | ruleAssertThat(code).hasLintViolationsWithoutAutoCorrect( 21 | LintViolation( 22 | line = 2, 23 | col = 5, 24 | detail = ${ruleName}.${ruleName}ErrorMessage, 25 | ), 26 | ) 27 | } 28 | 29 | @Test 30 | fun `passes for X case`() { 31 | @Language("kotlin") 32 | val code = 33 | """ 34 | TODO() 35 | """.trimIndent() 36 | ruleAssertThat(code).hasNoLintViolations() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/templates/Rule.kt.template: -------------------------------------------------------------------------------- 1 | package io.nlopez.compose.rules 2 | 3 | import io.nlopez.compose.core.ComposeKtVisitor 4 | 5 | class ${ruleName} : ComposeKtVisitor { 6 | 7 | companion object { 8 | val ${ruleName}ErrorMessage = """ 9 | TODO 10 | 11 | See https://mrmans0n.github.io/compose-rules/rules/#TODO for more information. 12 | """.trimIndent() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | plugins { 4 | id("com.gradle.develocity") version "4.0.2" 5 | } 6 | 7 | dependencyResolutionManagement { 8 | repositories { 9 | mavenCentral() 10 | } 11 | } 12 | 13 | develocity { 14 | buildScan { 15 | termsOfUseUrl = "https://gradle.com/terms-of-service" 16 | termsOfUseAgree = "yes" 17 | } 18 | } 19 | 20 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 21 | 22 | rootProject.name = "compose-rules" 23 | include( 24 | ":rules:common", 25 | ":rules:detekt", 26 | ":rules:ktlint", 27 | ) 28 | -------------------------------------------------------------------------------- /spotless/copyright.txt: -------------------------------------------------------------------------------- 1 | // Copyright $YEAR Nacho Lopez 2 | // SPDX-License-Identifier: Apache-2.0 3 | --------------------------------------------------------------------------------