diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7b37b615..c88a9d59a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 0.12.4 + +### 🐛 Bug Fixes + +* don't crash if Anvil isn't in the buildScript classpath by @RBusarow + in https://github.com/RBusarow/ModuleCheck/pull/944 +* ignore suppress/noinspection names which don't fit the FindingName pattern by @RBusarow + in https://github.com/RBusarow/ModuleCheck/pull/945 + +### Other Changes + +* GitHub release notes config by @RBusarow in https://github.com/RBusarow/ModuleCheck/pull/946 + +**Full Changelog**: https://github.com/RBusarow/ModuleCheck/compare/0.12.3...0.12.4 + # 0.12.3 ### 🐛 Bug Fixes @@ -149,7 +164,9 @@ - revert KaptMatcher name to `modulecheck.api.KaptMatcher` [@RBusarow](https://github.com/RBusarow) ([#613](https://github.com/rbusarow/ModuleCheck/pull/613)) - + delete `ConfiguredModule` [@RBusarow](https://github.com/RBusarow) ([#609](https://github.com/rbusarow/ModuleCheck/pull/609)) + - disable the "use tab character" option in IDE codestyle [@RBusarow](https://github.com/RBusarow) ([#607](https://github.com/rbusarow/ModuleCheck/pull/607)) - replace `java-test-fixtures` usages with `-testing` diff --git a/README.md b/README.md index 5ffd04b735..3979de24b1 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ pluginManagement { // top-level build.gradle.kts plugins { - id("com.rickbusarow.module-check") version "0.12.3" + id("com.rickbusarow.module-check") version "0.12.4" } ``` diff --git a/build-logic/mcbuild/src/main/kotlin/modulecheck/builds/publishing.kt b/build-logic/mcbuild/src/main/kotlin/modulecheck/builds/publishing.kt index 9fd2f3484d..04ca707f19 100644 --- a/build-logic/mcbuild/src/main/kotlin/modulecheck/builds/publishing.kt +++ b/build-logic/mcbuild/src/main/kotlin/modulecheck/builds/publishing.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Rick Busarow + * Copyright (C) 2021-2023 Rick Busarow * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -34,7 +34,7 @@ import org.jetbrains.dokka.gradle.AbstractDokkaLeafTask const val GROUP = "com.rickbusarow.modulecheck" const val PLUGIN_ID = "com.rickbusarow.module-check" -const val VERSION_NAME = "0.13.0-SNAPSHOT" +const val VERSION_NAME = "0.12.4" const val SOURCE_WEBSITE = "https://github.com/rbusarow/ModuleCheck" const val DOCS_WEBSITE = "https://rbusarow.github.io/ModuleCheck" diff --git a/website/src/pages/changelog.md b/website/src/pages/changelog.md index 5ece60d4c3..d2f608cb68 100644 --- a/website/src/pages/changelog.md +++ b/website/src/pages/changelog.md @@ -1,3 +1,18 @@ +## 0.12.4 + +#### 🐛 Bug Fixes + +* don't crash if Anvil isn't in the buildScript classpath by @RBusarow + in https://github.com/RBusarow/ModuleCheck/pull/944 +* ignore suppress/noinspection names which don't fit the FindingName pattern by @RBusarow + in https://github.com/RBusarow/ModuleCheck/pull/945 + +#### Other Changes + +* GitHub release notes config by @RBusarow in https://github.com/RBusarow/ModuleCheck/pull/946 + +**Full Changelog**: https://github.com/RBusarow/ModuleCheck/compare/0.12.3...0.12.4 + ## 0.12.3 #### 🐛 Bug Fixes @@ -149,7 +164,9 @@ - revert KaptMatcher name to `modulecheck.api.KaptMatcher` [@RBusarow](https://github.com/RBusarow) ([#613](https://github.com/rbusarow/ModuleCheck/pull/613)) - + delete `ConfiguredModule` [@RBusarow](https://github.com/RBusarow) ([#609](https://github.com/rbusarow/ModuleCheck/pull/609)) + - disable the "use tab character" option in IDE codestyle [@RBusarow](https://github.com/RBusarow) ([#607](https://github.com/rbusarow/ModuleCheck/pull/607)) - replace `java-test-fixtures` usages with `-testing` diff --git a/website/versioned_docs/version-0.12.4/ci_workflow.md b/website/versioned_docs/version-0.12.4/ci_workflow.md new file mode 100644 index 0000000000..5a410bf3b6 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/ci_workflow.md @@ -0,0 +1,145 @@ +--- +id: ci-workflow + +sidebar_label: CI Workflow + +title: CI Workflow +--- + +ModuleCheck will automatically fix most issues. Most CI platforms are able to commit changes, and +automatically cancel out-of-date jobs when the branch has been updated. This tooling can be used to +apply ModuleCheck's automatic fixes (if any) as part of a CI run, then cancel and start a new run. +This is similar to a git pre-commit hook, except the work is delegated to a build server. + +### Using CI over git hooks + +The traditional method for applying changes automatically is with a git hook, such as pre-commit or +pre-push. But if the task-to-be-automated has a runtime of more than a few seconds, this is a poor +developer experience. With a CI task, the execution is done automatically and asynchronously, while +the developer is already moving on to something else. + +A git hook also technically doesn't guarantee that a task is executed before code is checked in to a +main branch, since there's no guarantee that a hook is enabled. With CI, the task will output a +status check. If a branch protection rule is enabled, that status check can be required. This will +then guarantee that the task has run (successfully) before any code is checked in to the protected +branch. + +### Example Flow chart + +This is a simplified flowchart of how I would run ModuleCheck with unit tests in CI. The +cancellation, test, and ModuleCheck jobs run in parallel on three different runners. This is an +"optimistic" workflow, in that it assumes that the `modulecheck` task will not generate changes +which would trigger a restart. + +```mermaid +flowchart TB + Start(CI Start):::good --> mGraph + Start --> tGraph + Start --> cGraph + + subgraph mGraph [runner 1] + direction TB + ModuleCheck(./gradlew moduleCheckAuto):::code --> ChangesModuleCheck + ChangesModuleCheck{Graph changes?} --- yesM[yes]:::lineLabel --> CommitModuleCheck(Commit changes and push):::stop + ChangesModuleCheck --- noM[no]:::lineLabel --> EndModuleCheck("#10003;"):::good + end + + subgraph tGraph [runner 2] + direction TB + Tests(./gradlew test):::code --> EndTests("#10003;"):::good + end + + subgraph cGraph [runner 3] + direction TB + Cancel(Cancel previous CI run):::code + end + + style tGraph fill:#EEE,stroke:#000 + style cGraph fill:#EEE,stroke:#000 + style mGraph fill:#EEE,stroke:#000 + + classDef good fill:#0B0,stroke:#000 + classDef stop fill:#E33,stroke:#000 + + classDef code fill:#AAA,stroke:#000 + + style ChangesModuleCheck fill:#CD1,stroke:#000 + + classDef lineLabel fill:#FFF,stroke:#FFF +``` + +### Example GitHub Action + +Here's an Action which will run ModuleCheck, then commit any changes +using [Stefanzweifel's auto-commit](https://github.com/stefanzweifel/git-auto-commit-action). This +requires a personal access token secret, or the commit step will fail. + +```yaml title=.github/workflows.module-check.yml +name: ModuleCheck + +on: + pull_request: + +jobs: + + cancel-stale-jobs: + name: Cancel stale jobs + runs-on: ubuntu-latest + + steps: + # cancel previous jobs + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.0 + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + + ModuleCheck: + name: ModuleCheck + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.ref }} + # Must use a personal access token in order to commit changes + token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + fetch-depth: 0 + + - name: Set up JDK + uses : actions/setup-java@v2 + with : + distribution : 'temurin' + java-version : '11' + + # performs tree-shaking on the Gradle dependency graph + - name: modulecheck + run: ./gradlew moduleCheckAuto --no-daemon + + # If ModuleCheck generated changes, commit and push those changes. + # If there are no changes, then this is a no-op. + - name: commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Apply ModuleCheck changes + commit_options: '--no-verify --signoff' + + tests: + name: Unit tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up JDK + uses : actions/setup-java@v2 + with : + distribution : 'temurin' + java-version : '14' + + - name: all tests + run: ./gradlew test --no-daemon +``` diff --git a/website/versioned_docs/version-0.12.4/configuration.mdx b/website/versioned_docs/version-0.12.4/configuration.mdx new file mode 100644 index 0000000000..9a6ba8b2f2 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/configuration.mdx @@ -0,0 +1,170 @@ +--- +id: configuration +sidebar_label: Configuration +--- + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + + + + + + ``` kotlin title="root/build.gradle.kts" + plugins { + id("com.rickbusarow.module-check") version "0.13.0-SNAPSHOT" + } + + moduleCheck { + + deleteUnused = true // default is false + + checks { + overShotDependency = true // default is true + redundantDependency = false // default is false + unusedDependency = true // default is true + mustBeApi = true // default is true + inheritedDependency = true // default is true + sortDependencies = false // default is false + sortPlugins = false // default is false + unusedKapt = true // default is true + anvilFactoryGeneration = true // default is true + disableAndroidResources = false // default is false + disableViewBinding = false // default is false + unusedKotlinAndroidExtensions = false // default is false + depths = false // default is false + } + + // allow these modules to be declared as dependency anywhere, + // regardless of whether they're used + ignoreUnusedFinding = setOf(":test:core-jvm", ":test:core-android") + + // do not check the dependencies of these modules. + // in this case, :app could declare any module it wants without issue + doNotCheck = setOf(":app") + + additionalCodeGenerators = listOf( + modulecheck.config.CodeGeneratorBinding.AnnotationProcessor( + name = "My Processor", + generatorMavenCoordinates = "my-project.codegen:processor", + annotationNames = listOf( + "myproject.MyInject", + "myproject.MyInject.Factory", + "myproject.MyInjectParam", + "myproject.MyInjectModule" + ) + ) + ) + + reports { + checkstyle { + enabled = true // default is false + outputPath = "${project.buildDir}/reports/modulecheck/checkstyle.xml" + } + sarif { + enabled = true // default is false + outputPath = "${project.buildDir}/reports/modulecheck/modulecheck.sarif" + } + depths { + enabled = true // default is false + outputPath = "${project.buildDir}/reports/modulecheck/depths.txt" + } + graphs { + enabled = true // default is false + // The root directory of all generated graphs. If set, directories will be created + // for each module, mirroring the structure of the project. If this property is null, + // graphs will be created in the `build/reports/modulecheck/graphs/` relative + // directory of each project. + outputDir = "${project.buildDir}/reports/modulecheck/graphs" + } + text { + enabled = true // default is false + outputPath = "${project.buildDir}/reports/modulecheck/report.txt" + } + } + } + ``` + + + + + + ``` groovy title="root/build.gradle" + plugins { + id 'com.rickbusarow.module-check' version '0.13.0-SNAPSHOT' + } + + moduleCheck { + deleteUnused = true // default is false + + checks { + overShotDependency = true // default is true + redundantDependency = false // default is false + unusedDependency = true // default is true + mustBeApi = true // default is true + inheritedDependency = true // default is true + sortDependencies = false // default is false + sortPlugins = false // default is false + unusedKapt = true // default is true + anvilFactoryGeneration = true // default is true + disableAndroidResources = false // default is false + disableViewBinding = false // default is false + unusedKotlinAndroidExtensions = false // default is false + depths = false // default is false + } + + // allow these modules to be declared as dependency anywhere, + // regardless of whether they're used + ignoreUnusedFinding = [':test:core-jvm', ':test:core-android'] + + // do not check the dependencies of these modules. + // in this case, :app could declare any module it wants without issue + doNotCheck = [':app'] + + additionalCodeGenerators = [ + new modulecheck.config.CodeGeneratorBinding.AnnotationProcessor( + 'My Processor', + 'my-project.codegen:processor', + [ + "myproject.MyInject", + "myproject.MyInject.Factory", + "myproject.MyInjectParam", + "myproject.MyInjectModule" + ] + ) + ] + + reports { + checkstyle { + it.enabled = true // default is false + it.outputPath = "${project.buildDir}/reports/modulecheck/checkstyle.xml" + } + sarif { + it.enabled = true // default is false + it.outputPath = "${project.buildDir}/reports/modulecheck/modulecheck.sarif" + } + depths { + it.enabled = true // default is false + it.outputPath = "${project.buildDir}/reports/modulecheck/depths.txt" + } + graphs { + it.enabled = true // default is false + // The root directory of all generated graphs. If set, directories will be created + // for each module, mirroring the structure of the project. If this property is null, + // graphs will be created in the `build/reports/modulecheck/graphs/` relative + // directory of each project. + it.outputDir = "${project.buildDir}/reports/modulecheck/graphs" + } + text { + it.enabled = true // default is false + it.outputPath = "${project.buildDir}/reports/modulecheck/report.txt" + } + } + + } + ``` + + diff --git a/website/versioned_docs/version-0.12.4/quickstart.mdx b/website/versioned_docs/version-0.12.4/quickstart.mdx new file mode 100644 index 0000000000..c8afcaa2e6 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/quickstart.mdx @@ -0,0 +1,158 @@ +--- +id: quickstart +title: Quick Start +sidebar_label: Quick Start +slug: / +--- + + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + + +## Dependencies + + + + + + ```kotlin + // settings.gradle.kts + + pluginManagement { + repositories { + gradlePluginPortal() + // Add for SNAPSHOT builds + maven("https://oss.sonatype.org/content/repositories/snapshots/") + } + } + ``` + + ```kotlin + // top-level build.gradle.kts + + plugins { + id("com.rickbusarow.module-check") version "0.13.0-SNAPSHOT" + } + ``` + + + + + + ```groovy + // settings.gradle + + pluginManagement { + repositories { + gradlePluginPortal() + // Add for SNAPSHOT builds + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } + } + } + ``` + + ```groovy + // top-level build.gradle + + plugins { + id 'com.rickbusarow.module-check' version '0.13.0-SNAPSHOT' + } + ``` + + + + +## Tasks + +all checks +```shell +./gradlew moduleCheck +``` + +all checks with auto-correct +```shell +./gradlew moduleCheckAuto +``` + +check sorting +```shell +./gradlew moduleCheckSortPlugins moduleCheckSortDependencies +``` + +apply sorting +```shell +./gradlew moduleCheckSortPluginsAuto moduleCheckSortDependenciesAuto +``` + +report depths of each module +```shell +./gradlew moduleCheckDepths +``` + +generate (module-only) dependency graphs +```shell +./gradlew moduleCheckGraphs +``` + +## Configuration + +See [configuration](./configuration.mdx) for a full list of options. + + + + + + + ```kotlin title="root/build.gradle.kts" + configure + { + + alwaysIgnore.set(setOf(":app")) + + checks { + redundant.set(false) + } + } + ``` + + --or-- + + ```kotlin title="root/build.gradle.kts" + moduleCheck { + + alwaysIgnore.set(setOf(":app")) + + checks { + redundant.set(false) + } + } + ``` + + + + + + + ```groovy title="root/build.gradle" + moduleCheck { + + alwaysIgnore.set(setOf(":app")) + + checks { + redundant.set(false) + } + } + ``` + + + diff --git a/website/versioned_docs/version-0.12.4/rules/android/disable_android_resources.md b/website/versioned_docs/version-0.12.4/rules/android/disable_android_resources.md new file mode 100644 index 0000000000..71ec297305 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/android/disable_android_resources.md @@ -0,0 +1,17 @@ +--- +id: disable_android_resources +slug: /rules/disable_android_resources +title: Disable Android Resources +sidebar_label: Disable Android Resources +--- + +If an Android module doesn't actually have any resources in the `src/__/res` directory, +then `android.buildFeatures.androidResources` can be disabled. + +```kotlin +android { + buildFeatures { + androidResource = false + } +} +``` diff --git a/website/versioned_docs/version-0.12.4/rules/android/disable_view_binding.md b/website/versioned_docs/version-0.12.4/rules/android/disable_view_binding.md new file mode 100644 index 0000000000..45a27be804 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/android/disable_view_binding.md @@ -0,0 +1,17 @@ +--- +id: disable_view_binding +slug: /rules/disable_view_binding +title: Disable ViewBinding +sidebar_label: Disable ViewBinding +--- + +If an Android module has `viewBinding` enabled, but doesn't contribute any generated `____Binding` +objects from layout files which are actually used, then `viewBinding` can be disabled. + +```kotlin +android { + buildFeatures { + viewBinding = false + } +} +``` diff --git a/website/versioned_docs/version-0.12.4/rules/android/unused_kotlin_android_extensions.md b/website/versioned_docs/version-0.12.4/rules/android/unused_kotlin_android_extensions.md new file mode 100644 index 0000000000..1c376f3c86 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/android/unused_kotlin_android_extensions.md @@ -0,0 +1,9 @@ +--- +id: unused_kotlin_android_extensions +slug: /rules/unused_kotlin_android_extensions +title: Unused Kotlin Android Extensions +sidebar_label: Unused Kotlin Android Extensions +--- + +Finds modules which have deprecated Kotlin Android Extensions enabled, but don't actually use any +synthetic imports or deprecated @Parcelize annotation diff --git a/website/versioned_docs/version-0.12.4/rules/compiler/custom_kapt_matchers.md b/website/versioned_docs/version-0.12.4/rules/compiler/custom_kapt_matchers.md new file mode 100644 index 0000000000..f6323ac4b8 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/compiler/custom_kapt_matchers.md @@ -0,0 +1,30 @@ +--- +id: custom_kapt_matchers +slug: /rules/custom_kapt_matchers +title: Custom Kapt Matchers +sidebar_label: Custom Kapt Matchers +--- + +It's simple to add a custom matcher for an internal-use annotation processor. + +Just define a list of regex strings for all of the fully qualified names of its annotations. + +```kotlin +moduleCheck { + additionalKaptMatchers.set( + listOf( + modulecheck.api.KaptMatcher( + name = "MyProcessor", + processor = "my-project.codegen:processor", + annotationImports = listOf( + "myproject\\.\\*", + "myproject\\.MyInject", + "myproject\\.MyInject\\.Factory", + "myproject\\.MyInjectParam", + "myproject\\.MyInjectModule" + ) + ) + ) + ) +} +``` diff --git a/website/versioned_docs/version-0.12.4/rules/compiler/unused_kapt_plugin.md b/website/versioned_docs/version-0.12.4/rules/compiler/unused_kapt_plugin.md new file mode 100644 index 0000000000..9a45eadfa1 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/compiler/unused_kapt_plugin.md @@ -0,0 +1,9 @@ +--- +id: unused_kapt_plugin +slug: /rules/unused_kapt_plugin +title: Unused Kapt Plugin +sidebar_label: Unused Kapt Plugin +--- + +If there are no `kapt`/`kaptTest`/etc. processor dependencies in a module, there's no point in +applying the `org.jetbrains.kotlin.kapt` plugin. diff --git a/website/versioned_docs/version-0.12.4/rules/compiler/unused_kapt_processor.md b/website/versioned_docs/version-0.12.4/rules/compiler/unused_kapt_processor.md new file mode 100644 index 0000000000..325423fd94 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/compiler/unused_kapt_processor.md @@ -0,0 +1,31 @@ +--- +id: unused_kapt_processor +slug: /rules/unused_kapt_processor +title: Unused Kapt Processor +sidebar_label: Unused Kapt Processor +--- + +Annotation processors act upon a defined set of annotations. If an annotation processor is +sufficiently popular and its api is stable, then it's relatively simple to define a list of +annotations to search for. For instance, Dagger looks for the following annotations: + +- `javax.inject.Inject` +- `dagger.Binds` +- `dagger.Module` +- `dagger.multibindings.IntoMap` +- `dagger.multibindings.IntoSet` +- `dagger.BindsInstance` +- `dagger.Component` +- `dagger.assisted.Assisted` +- `dagger.assisted.AssistedInject` +- `dagger.assisted.AssistedFactory` +- `com.squareup.anvil.annotations.ContributesTo` +- `com.squareup.anvil.annotations.MergeComponent` +- `com.squareup.anvil.annotations.MergeSubomponent` + +If a module has the Dagger `kapt` dependency, and that module *does not* have one of the above +annotations somewhere, then Dagger isn't actually doing anything and can be removed. + +This is simply a best-effort approach, and it isn't maintenance-free. Over time, the list of +annotations for any processor may change. If this rule gives a false-positive finding because of a +new annotation, please open an issue and/or pull request. diff --git a/website/versioned_docs/version-0.12.4/rules/compiler/use_anvil_factory_generation.md b/website/versioned_docs/version-0.12.4/rules/compiler/use_anvil_factory_generation.md new file mode 100644 index 0000000000..681ca679d9 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/compiler/use_anvil_factory_generation.md @@ -0,0 +1,19 @@ +--- +id: use_anvil_factory_generation +slug: /rules/use_anvil_factory_generation +title: Could Use Anvil Factory Generation +sidebar_label: Could Use Anvil Factory Generation +--- + +Anvil's [factory generation](https://github.com/square/anvil#dagger-factory-generation) is faster +than Dagger's generation using Kapt. However, it doesn't support generating Components or +Subcomponents, and it doesn't work in Java code. + +This rule detects whether a module could switch from Dagger's kapt to Anvil factory generation. + +Criteria: + +- Anvil plugin applied with a version greater than 2.0.11 +- Anvil's factory generation isn't already enabled (nothing to do in this case) +- No `@MergeComponent`, `@MergeSubcomponent`, `@Component` or `@Subcomponent` annotations +- No Dagger annotations in `.java` files diff --git a/website/versioned_docs/version-0.12.4/rules/inherited_dependency.md b/website/versioned_docs/version-0.12.4/rules/inherited_dependency.md new file mode 100644 index 0000000000..3c84cd7625 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/inherited_dependency.md @@ -0,0 +1,12 @@ +--- +id: inherited_dependency +slug: /rules/inherited_dependency +title: Inherited Dependency +sidebar_label: Inherited Dependency +--- + +Assume that `:moduleA` depends upon `:moduleB`, and `:moduleB` depends upon `:moduleC` via +an `api` configuration. Also assume that `:moduleA` uses something from `:moduleC`, but doesn't +have an explicit dependency for it. It just inherits that dependency from `:moduleB`. + +ModuleCheck will recommend adding a direct, explicit dependency for `:moduleA` -> `:moduleC`. diff --git a/website/versioned_docs/version-0.12.4/rules/must_be_api.md b/website/versioned_docs/version-0.12.4/rules/must_be_api.md new file mode 100644 index 0000000000..c650fdba66 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/must_be_api.md @@ -0,0 +1,14 @@ +--- +id: must_be_api +slug: /rules/must_be_api +title: Must Be Api +sidebar_label: Must Be Api +--- + +Dependencies are considered to be part of a module's public "ABI" if that module exposes some aspect +of the dependency in its own API. + +For instance, if a `:moduleA` extends a class/interface from `:moduleB`, or takes a type +from `:moduleB` as a function parameter, then any consumer of `:moduleA`'s API must also have a +dependency upon `:moduleB`. In scenarios like this, the dependency module(s) should be declared +using Gradle's `api` configuration. diff --git a/website/versioned_docs/version-0.12.4/rules/overshot_dependency.md b/website/versioned_docs/version-0.12.4/rules/overshot_dependency.md new file mode 100644 index 0000000000..0dc9a3fdeb --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/overshot_dependency.md @@ -0,0 +1,27 @@ +--- +id: overshot_dependency +slug: /rules/overshot_dependency +title: Overshot Dependency +sidebar_label: Overshot Dependency +--- + +Finds project dependencies which aren't used by the declaring configuration, but are used by a +dependent, downstream configuration. + +For instance, assume that `:moduleB` declares an `implementation` dependency upon `:moduleA`. + +```kotlin title="moduleB/build.gradle.kts" +dependencies { + implementation(project(":moduleA")) +} +``` + +If `:moduleB` doesn't actually use `:moduleA` in its `main` source, but it _does_ use it in `test` +source, it's an __overshot dependency__. The declaration should be changed to +use `testImplementation`: + +```kotlin title="moduleB/build.gradle.kts" +dependencies { + testImplementation(project(":moduleA")) +} +``` diff --git a/website/versioned_docs/version-0.12.4/rules/project_depth.md b/website/versioned_docs/version-0.12.4/rules/project_depth.md new file mode 100644 index 0000000000..11b79760bd --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/project_depth.md @@ -0,0 +1,194 @@ +--- +id: project_depth +slug: /rules/project_depth +title: Project Depth +sidebar_label: Project Depth +--- + +TL;DR - Low depth values mean faster builds and better all-around scalability. + +--- + +It's often useful to think of module dependencies as a directed tree +or [directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph). If a module is a +node, then each module dependency is a child node, and the dependencies of those dependencies are +grand-child nodes. + +This is especially useful when thinking about **build performance**, because the parent-child +relationship is clear: _child nodes must build before parent nodes_. + +```mermaid +flowchart TB + + classDef depth2 fill:#BBF,stroke:#000,color:#000 + classDef depth1 fill:#B9B,stroke:#000,color:#000 + classDef depth0 fill:#FBB,stroke:#000,color:#000 + + linkStyle default stroke-width:2px,fill:none,stroke:green; + + app(:app):::depth2 + + screen1(:screen-1):::depth1 + screen2(:screen-2):::depth1 + + lib1(:lib-1):::depth0 + lib2(:lib-2):::depth0 + + app --> screen1 + app --> screen2 + + screen1 --> lib1 + screen1 --> lib2 + screen2 --> lib2 +``` + +In the above example, + +- `:lib-1` and `:lib-2` must be built before `:screen-1`. +- `:lib-2` must be build before `:screen-2`. +- `:screen-1` and `:screen-2` must be built before `:app`. + +It's worth pointing out that this relationship is recursive, as well. Grand-child nodes must build +before their parents. + +### Dependencies and Build Concurrency + +Individual module builds are always done single-threaded, but multiple modules may build in parallel +so long as no module in the set depends upon another module in that set. In the above graph, + +- `:lib-1` and `:lib-2` may build in parallel +- `:lib-1` and `:screen-2` may build in parallel +- `:scren-1` and `:screen-2` may build in parallel + +The maximum number of parallel module builds is determined by the structure of the dependency graph +and the number of available processor cores on the machine which is performing the build. + +### Depth + +**Depth** refers to the maximum number of edges between a module and each of its leaf nodes in the +project dependency graph. + +Low depth values indicate a shallow or flat project structure with loose (or no) coupling between +modules. In a full build, these projects scale well with hardware upgrades because they're able to +build all those independent modules in parallel. + +```mermaid +flowchart TB + + subgraph sg [A shallow graph] + direction TB + + classDef depth3 fill:#F7B,stroke:#000,color:#000 + classDef depth2 fill:#BBF,stroke:#000,color:#000 + classDef depth1 fill:#B9B,stroke:#000,color:#000 + classDef depth0 fill:#FBB,stroke:#000,color:#000 + + linkStyle default stroke-width:2px,fill:none,stroke:green; + + app(depth: 2):::depth2 + + screen1(depth: 1):::depth1 + screen2(depth: 1):::depth1 + screen3(depth: 1):::depth1 + screen4(depth: 1):::depth1 + + lib1(depth: 0):::depth0 + lib2(depth: 0):::depth0 + lib3(depth: 0):::depth0 + lib4(depth: 0):::depth0 + lib5(depth: 0):::depth0 + + app --> screen1 + app --> screen2 + app --> screen3 + app --> screen4 + + screen1 --> lib1 + screen1 --> lib4 + + screen2 --> lib1 + screen2 --> lib3 + screen2 --> lib4 + + screen3 --> lib2 + screen3 --> lib3 + screen3 --> lib4 + + screen4 --> lib3 + screen4 --> lib5 + + end + + style sg opacity:0.0 + +``` + +On the other hand, "deep" projects do not offer many opportunities for parallelization. They have +project dependencies which must be built *sequentially*. They also perform poorly in incremental +builds, because a single change to even a mid-level module invalidates cached builds for half of the +project. + +```mermaid +flowchart TB + + style sg opacity:0.0 + subgraph sg [A deep graph] + direction TB + + classDef depth6 fill:#800,stroke:#000,color:#FFF + classDef depth5 fill:#A50,stroke:#000,color:#FFF + classDef depth4 fill:#C0B,stroke:#000,color:#000 + classDef depth3 fill:#F7B,stroke:#000,color:#000 + classDef depth2 fill:#BBF,stroke:#000,color:#000 + classDef depth1 fill:#B9B,stroke:#000,color:#000 + classDef depth0 fill:#FBB,stroke:#000,color:#000 + + linkStyle default stroke-width:2px,fill:none,stroke:green; + + app(depth: 6):::depth6 + + screen1(depth: 5):::depth5 + screen2(depth: 5):::depth5 + + screen3(depth: 4):::depth4 + screen4(depth: 4):::depth4 + + lib1(depth: 3):::depth3 + lib2(depth: 3):::depth3 + + lib3(depth: 2):::depth2 + lib4(depth: 2):::depth2 + + lib5(depth: 1):::depth1 + + lib6(depth: 0):::depth0 + + app --> screen1 + app --> screen2 + app --> screen3 + app --> screen4 + + screen1 --> screen3 + screen1 --> screen4 + + screen2 --> screen4 + + screen3 --> lib1 + screen3 --> lib2 + + screen4 --> lib1 + screen4 --> lib4 + + lib1 --> lib3 + lib1 --> lib4 + + lib2 --> lib3 + + lib3 --> lib5 + lib4 --> lib5 + + lib5 --> lib6 + + end + +``` diff --git a/website/versioned_docs/version-0.12.4/rules/redundant_dependency.md b/website/versioned_docs/version-0.12.4/rules/redundant_dependency.md new file mode 100644 index 0000000000..92a0da6bf7 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/redundant_dependency.md @@ -0,0 +1,67 @@ +--- +id: redundant_dependency +slug: /rules/redundant_dependency +title: Redundant Dependency +sidebar_label: Redundant Dependency +--- +:::caution + +This rule creates a brittle dependency graph, because some necessary dependencies are only provided +transitively by other dependencies. Any manual changes to dependencies can have unexpected +consequences downstream. + +This rule is **not recommended** and disabled by default, but it's still available for those who +want to keep their build files as small as possible. + +::: + +Finds project dependencies which are declared as `api` in other dependency projects, but also +declared in the current project. These dependencies can be removed without actually breaking the +build, since they're still provided by an upstream dependency through the `api` configuration. + +```mermaid +flowchart LR + + linkStyle default stroke-width:2px,fill:none,stroke:green; + + classDef depth2 fill:#BBF,stroke:#000,color:#000 + classDef depth1 fill:#B9B,stroke:#000,color:#000 + classDef depth0 fill:#FBB,stroke:#000,color:#000 + + subgraph sg_redundant [A redundant graph] + direction TB + + lib1_redundant(:lib-1):::depth0 + lib2_redundant(:lib-2):::depth1 + app_redundant(:app):::depth2 + + app_redundant --> |api| lib1_redundant + app_redundant --> |api| lib2_redundant + + lib2_redundant --> |api| lib1_redundant + end + + subgraph sg_minimalist [A graph with no redundancy] + direction TB + + lib1_minimalist(:lib-1):::depth0 + lib2_minimalist(:lib-2):::depth1 + app_minimalist(:app):::depth2 + + app_minimalist --> |api| lib2_minimalist + + lib2_minimalist --> |api| lib1_minimalist + end + + style sg_redundant fill:#C66,stroke:#000,color:#FFF + style sg_minimalist fill:#696,stroke:#000,color:#FFF + + sg_redundant --> |./gradlew moduleCheck| sg_minimalist + +``` + +This is the opposite of the [inherited dependency] rule, which ensures a stable graph by explicitly +declaring each dependency. [Inherited dependency] is enabled by default, and is the recommended +approach. Both rules may not be enabled at the same time. + +[Inherited dependency]:inherited_dependency.md diff --git a/website/versioned_docs/version-0.12.4/rules/sorting/sort_dependencies.md b/website/versioned_docs/version-0.12.4/rules/sorting/sort_dependencies.md new file mode 100644 index 0000000000..34e6f7feee --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/sorting/sort_dependencies.md @@ -0,0 +1,6 @@ +--- +id: sort_dependencies +slug: /rules/sort_dependencies +title: Sort Dependencies +sidebar_label: Sort Dependencies +--- diff --git a/website/versioned_docs/version-0.12.4/rules/sorting/sort_plugins.md b/website/versioned_docs/version-0.12.4/rules/sorting/sort_plugins.md new file mode 100644 index 0000000000..c293df4633 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/sorting/sort_plugins.md @@ -0,0 +1,6 @@ +--- +id: sort_plugins +slug: /rules/sort_plugins +title: Sort Plugins +sidebar_label: Sort Plugins +--- diff --git a/website/versioned_docs/version-0.12.4/rules/unused_dependency.md b/website/versioned_docs/version-0.12.4/rules/unused_dependency.md new file mode 100644 index 0000000000..b8285c7c73 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/rules/unused_dependency.md @@ -0,0 +1,14 @@ +--- +id: unused_dependency +slug: /rules/unused_dependency +title: Unused Dependency +sidebar_label: Unused Dependency +--- + +Unused module dependencies which are unused create unnecessary bottlenecks in a build task. Instead +of building modules concurrently, Gradle must wait until the dependency module is built before +beginning to build the dependent one. + +ModuleCheck determines whether a dependency is unused by looking for all fully qualified names +declared in its API, then searching the dependent module's code for references to any of those +names. If there are no references, the dependency module is considered to be unused. diff --git a/website/versioned_docs/version-0.12.4/suppressing-findings.mdx b/website/versioned_docs/version-0.12.4/suppressing-findings.mdx new file mode 100644 index 0000000000..3f04ea26c1 --- /dev/null +++ b/website/versioned_docs/version-0.12.4/suppressing-findings.mdx @@ -0,0 +1,66 @@ +--- +id: suppressing-findings +title: Suppressing Findings +sidebar_label: Suppressing Findings +--- + + +import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + +You can disable individual ModuleCheck findings via annotation, just like with any other lint tool. + +The name of the check to disable can be found in the `name` column of console output: + +``` +> Task :moduleCheck +ModuleCheck found 3 issues in 6.157 seconds + +:app + dependency name build file + :fat-and-leaky inherited-dependency /Users/rbusarow/projects/sample/app/build.gradle.kts: (15, 3): + :fat-and-leaky must-be-api /Users/rbusarow/projects/sample/app/build.gradle.kts: (15, 3): + :unused-lib unused-dependency /Users/rbusarow/projects/sample/app/build.gradle.kts: (49, 3): + +``` + + + + + + ```kotlin title="build.gradle.kts" + @Suppress("must-be-api") // don't switch anything to an api config + dependencies { + + @Suppress("unused-dependency") // don't comment out or delete this dependency + implementation(project(":unused-lib")) + + @Suppress("inherited-dependency") // don't add dependencies which are inherited from this library + implementation(project(":leaky")) + } + ``` + + + + + + ```groovy title="build.gradle" + // don't switch anything to an api config + //noinspection must-be-api + dependencies { + + // don't comment out or delete this dependency + //noinspection unused-dependency + implementation(project(":unused-lib")) + + // don't add dependencies which are inherited from this library + //noinspection inherited-dependency + implementation(project(":leaky")) + } + ``` + + + diff --git a/website/versioned_sidebars/version-0.12.4-sidebars.json b/website/versioned_sidebars/version-0.12.4-sidebars.json new file mode 100644 index 0000000000..f7a5440a8d --- /dev/null +++ b/website/versioned_sidebars/version-0.12.4-sidebars.json @@ -0,0 +1,51 @@ +{ + "Docs": [ + "quickstart", + "configuration", + "suppressing-findings", + "ci-workflow", + { + "type": "category", + "label": "Rules", + "collapsed": false, + "items": [ + "rules/unused_dependency", + "rules/must_be_api", + "rules/inherited_dependency", + "rules/redundant_dependency", + "rules/overshot_dependency", + "rules/project_depth", + { + "type": "category", + "label": "Compiler", + "collapsed": false, + "items": [ + "rules/compiler/use_anvil_factory_generation", + "rules/compiler/unused_kapt_processor", + "rules/compiler/unused_kapt_plugin", + "rules/compiler/custom_kapt_matchers" + ] + }, + { + "type": "category", + "label": "Sorting", + "collapsed": false, + "items": [ + "rules/sorting/sort_dependencies", + "rules/sorting/sort_plugins" + ] + }, + { + "type": "category", + "label": "Android", + "collapsed": false, + "items": [ + "rules/android/disable_android_resources", + "rules/android/disable_view_binding", + "rules/android/unused_kotlin_android_extensions" + ] + } + ] + } + ] +} diff --git a/website/versions.json b/website/versions.json index 62db22e983..6fce7751cf 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "0.12.4", "0.12.3", "0.12.2", "0.12.1",