Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
formatting:
name: Formatting
name: Formatting & Static Analysis
runs-on: ubuntu-latest
steps:
- name: Check out code
Expand All @@ -20,6 +20,8 @@ jobs:
java-version: 21
- name: spotless
run: ./gradlew spotlessCheck
- name: detekt
run: ./gradlew detekt

sample-app:
name: Sample App
Expand Down Expand Up @@ -62,3 +64,6 @@ jobs:

- name: Validate plugin project configuration
run: ./gradlew --stacktrace compose-guard:check

- name: Coverage report
run: ./gradlew --stacktrace koverXmlReport koverVerify
50 changes: 50 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Deploy docs

on:
push:
branches: [main]
paths:
- 'website/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: website
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-pages-artifact@v3
with:
path: website/out

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
25 changes: 25 additions & 0 deletions app/detekt-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Designed and developed by 2025 androidpoet (Ranbir Singh)

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

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

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>EmptyFunctionBlock:NamingRulesDemo.kt${ }</ID>
<ID>EmptyFunctionBlock:NamingRulesDemo.kt${}</ID>
<ID>EmptyFunctionBlock:ParameterRulesDemo.kt${ }</ID>
<ID>UnusedPrivateMember:MainActivity.kt$@Preview(showBackground = true) @Composable private fun ComposeGuardSampleAppPreview()</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fun BadEvent(onClick: () -> Unit, modifier: Modifier = Modifier) {

@Preview
@Composable
fun GoodEvent(onClick: () -> Unit, modifier: Modifier = Modifier) {
fun GoodEvent(onClick: () -> Unit, modifier: Modifier = Modifier) {
Button(onClick = onClick, modifier = modifier) { Text("Click") }
}

Expand All @@ -89,7 +89,7 @@ fun BFormField(
errorMessage: String = "",
isError: Boolean = false,
enabled: Boolean = true,
placeholder: String = ""
placeholder: String = "",
) {
}

Expand Down
47 changes: 47 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.spotless)
alias(libs.plugins.detekt) apply false
alias(libs.plugins.kover)
}

// Modules whose test coverage we aggregate and report on. The IntelliJ plugin
// is the unit-testable core; the Android sample is excluded from coverage.
val coveredModules = listOf("compose-guard")

subprojects {
tasks.withType<KotlinCompile> {
compilerOptions {
Expand All @@ -18,6 +24,29 @@ subprojects {

apply(plugin = rootProject.libs.plugins.spotless.get().pluginId)

// Static analysis: detekt with the shared config in config/detekt/detekt.yml.
apply(plugin = rootProject.libs.plugins.detekt.get().pluginId)
configure<io.gitlab.arturbosch.detekt.extensions.DetektExtension> {
parallel = true
buildUponDefaultConfig = true
config.setFrom(rootProject.files("config/detekt/detekt.yml"))
baseline = file("detekt-baseline.xml")
source.setFrom(
files(
"src/main/kotlin",
"src/test/kotlin",
).filter { it.exists() },
)
}
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = "17"
reports {
html.required.set(true)
xml.required.set(true)
sarif.required.set(true)
}
}

configure<com.diffplug.gradle.spotless.SpotlessExtension> {
kotlin {
target("**/*.kt")
Expand All @@ -43,3 +72,21 @@ subprojects {
}
}
}

// Aggregate test coverage across the covered modules. Run `./gradlew koverXmlReport`
// (machine-readable) or `./gradlew koverHtmlReport` (browsable), and `koverVerify`
// to enforce the floor below.
dependencies {
coveredModules.forEach { kover(project(":$it")) }
}

kover {
reports {
verify {
rule("Aggregate line coverage") {
// Floor for the PSI rule engine + settings logic (currently ~68%).
minBound(50)
}
}
}
}
1 change: 1 addition & 0 deletions compose-guard/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ plugins {
kotlin("jvm")
id("org.jetbrains.intellij.platform") version "2.10.1"
id(libs.plugins.spotless.get().pluginId)
id(libs.plugins.kover.get().pluginId)
}

kotlin {
Expand Down
66 changes: 66 additions & 0 deletions compose-guard/detekt-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Designed and developed by 2025 androidpoet (Ranbir Singh)

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

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

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:ModifierTopMostRule.kt$ModifierTopMostRule$(argName == "modifier" || argName == null) &amp;&amp; (argExpr == modifierName || argExpr.startsWith("$modifierName."))</ID>
<ID>CyclomaticComplexMethod:ContentSlotReusedRule.kt$ContentSlotReusedRule$override fun doAnalyze(function: KtNamedFunction, context: AnalysisContext): List&lt;ComposeRuleViolation&gt;</ID>
<ID>CyclomaticComplexMethod:LazyListContentTypeRule.kt$LazyListContentTypeRule$override fun doAnalyze( function: KtNamedFunction, context: AnalysisContext, ): List&lt;ComposeRuleViolation&gt;</ID>
<ID>CyclomaticComplexMethod:ModifierReuseRule.kt$ModifierReuseRule$override fun doAnalyze( function: KtNamedFunction, context: AnalysisContext, ): List&lt;ComposeRuleViolation&gt;</ID>
<ID>EmptyCatchBlock:ComposeGuardAnnotator.kt$ComposeGuardAnnotator${ }</ID>
<ID>EmptyCatchBlock:ComposeGuardInlayHintsProvider.kt${ }</ID>
<ID>EmptyCatchBlock:ComposeGuardInspection.kt$ComposeGuardInspection.&lt;no name provided&gt;${ }</ID>
<ID>EmptyCatchBlock:ComposeGuardLineMarkerProvider.kt$ComposeGuardLineMarkerProvider${ }</ID>
<ID>EmptyCatchBlock:ComposeGuardStatisticsService.kt$ComposeGuardStatisticsService.&lt;no name provided&gt;${ }</ID>
<ID>ImplicitDefaultLocale:DeadRuleSweepTest.kt$DeadRuleSweepTest$String.format("%-28s %s%n", rule.id, if (count == 0) "*** ZERO ***" else count.toString())</ID>
<ID>LoopWithTooManyJumpStatements:AddContentTypeFix.kt$AddContentTypeFix$for</ID>
<ID>LoopWithTooManyJumpStatements:AddKeyParameterFix.kt$AddKeyParameterFix$for</ID>
<ID>LoopWithTooManyJumpStatements:ComposeRuleExtensions.kt$for</ID>
<ID>LoopWithTooManyJumpStatements:ContentSlotReusedRule.kt$ContentSlotReusedRule$for</ID>
<ID>LoopWithTooManyJumpStatements:DerivedStateOfCandidateRule.kt$DerivedStateOfCandidateRule$for</ID>
<ID>LoopWithTooManyJumpStatements:EffectKeysRule.kt$EffectKeysRule$for</ID>
<ID>LoopWithTooManyJumpStatements:EventParameterNamingRule.kt$EventParameterNamingRule$for</ID>
<ID>LoopWithTooManyJumpStatements:ExplicitDependenciesRule.kt$ExplicitDependenciesRule$for</ID>
<ID>LoopWithTooManyJumpStatements:FrequentRecompositionRule.kt$FrequentRecompositionRule$for</ID>
<ID>LoopWithTooManyJumpStatements:HoistStateRule.kt$HoistStateRule$for</ID>
<ID>LoopWithTooManyJumpStatements:LambdaParameterInEffectRule.kt$LambdaParameterInEffectRule$for</ID>
<ID>LoopWithTooManyJumpStatements:LazyListContentTypeRule.kt$LazyListContentTypeRule$for</ID>
<ID>LoopWithTooManyJumpStatements:LazyListMissingKeyRule.kt$LazyListMissingKeyRule$for</ID>
<ID>LoopWithTooManyJumpStatements:Material2Rule.kt$Material2Rule$for</ID>
<ID>LoopWithTooManyJumpStatements:ModifierOrderRule.kt$ModifierOrderRule$for</ID>
<ID>LoopWithTooManyJumpStatements:ModifierTopMostRule.kt$ModifierTopMostRule$for</ID>
<ID>LoopWithTooManyJumpStatements:MovableContentRule.kt$MovableContentRule$for</ID>
<ID>LoopWithTooManyJumpStatements:MoveModifierToRootFix.kt$MoveModifierToRootFix$for</ID>
<ID>LoopWithTooManyJumpStatements:MutableStateParameterRule.kt$MutableStateParameterRule$for</ID>
<ID>LoopWithTooManyJumpStatements:ParameterOrderingRule.kt$ParameterOrderingRule$for</ID>
<ID>LoopWithTooManyJumpStatements:RememberStateRule.kt$RememberStateRule$for</ID>
<ID>LoopWithTooManyJumpStatements:ViewModelForwardingRule.kt$ViewModelForwardingRule$for</ID>
<ID>MaxLineLength:LambdaParameterInEffectRule.kt$LambdaParameterInEffectRule$override val documentationUrl: String = "https://mrmans0n.github.io/compose-rules/latest/rules/#lambdas-used-in-restartable-effects-should-be-checked"</ID>
<ID>MaxLineLength:TrailingLambdaRule.kt$TrailingLambdaRule$override val documentationUrl: String = "https://mrmans0n.github.io/compose-rules/latest/rules/#slots-for-main-content-should-be-the-trailing-lambda"</ID>
<ID>NestedBlockDepth:LazyListContentTypeRule.kt$LazyListContentTypeRule$override fun doAnalyze( function: KtNamedFunction, context: AnalysisContext, ): List&lt;ComposeRuleViolation&gt;</ID>
<ID>SpreadOperator:ComposeGuardInspection.kt$ComposeGuardInspection.&lt;no name provided&gt;$( violation.element, "[${rule.id}] ${violation.message}", violation.highlightType, *violation.quickFixes.toTypedArray(), )</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "item { Text(\"Header\") }"</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "item(content = { Text(\"Header\") })"</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "item(key = \"myKey\") { Text(\"Header\") }"</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "items(users) { Text(it.name) }"</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "items(users, key = { it.id }) { Text(it.name) }"</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "itemsIndexed(users) { index, user -&gt; Text(user.name) }"</ID>
<ID>UnusedPrivateProperty:AddContentTypeFixTest.kt$AddContentTypeFixTest$val inputDescription = "stickyHeader { Text(\"Header\") }"</ID>
<ID>UnusedPrivateProperty:AddLambdaAsEffectKeyFix.kt$AddLambdaAsEffectKeyFix$val trailingLambda = effectCall.lambdaArguments.firstOrNull()?.getLambdaExpression() ?: effectCall.valueArguments.lastOrNull()?.getArgumentExpression() as? KtLambdaExpression</ID>
<ID>UnusedPrivateProperty:HoistStateRule.kt$HoistStateRule$private val containerComposables = setOf( "Box", "Column", "Row", "Surface", "Card", "Scaffold", "LazyColumn", "LazyRow", "LazyVerticalGrid", "LazyHorizontalGrid", "ConstraintLayout", "FlowRow", "FlowColumn", "BoxWithConstraints", "AlertDialog", "Dialog", "ModalBottomSheet", "BottomSheet", )</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ public class ExplicitDependenciesRule : ComposableFunctionRule() {
),
)
}

}

// A CompositionLocal read is `LocalFoo.current` — a property access, NOT a call. Scanning only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ class AddContentTypeFixBehaviorTest : BasePlatformTestCase() {
.first { it.calleeExpression?.text == callee }
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
call.calleeExpression ?: call, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
call.calleeExpression ?: call,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) { fix.applyFix(project, descriptor) }
return file.text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ class AddExplicitParameterFixBehaviorTest : BasePlatformTestCase() {
private fun apply(fix: LocalQuickFix, target: PsiElement): String {
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
target, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
target,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) { fix.applyFix(project, descriptor) }
return myFixture.file.text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ class AddKeyParameterFixBehaviorTest : BasePlatformTestCase() {
.first { it.calleeExpression?.text == "items" || it.calleeExpression?.text == "itemsIndexed" }
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
call.calleeExpression ?: call, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
call.calleeExpression ?: call,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) { fix.applyFix(project, descriptor) }
return file.text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,12 @@ class AddModifierParameterFixBehaviorTest : BasePlatformTestCase() {
private fun applyFix(fix: LocalQuickFix, target: PsiElement) {
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
target, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
target,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) {
fix.applyFix(project, descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ class EffectFixBehaviorTest : BasePlatformTestCase() {
private fun apply(fix: LocalQuickFix, target: PsiElement): String {
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
target, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
target,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) { fix.applyFix(project, descriptor) }
return myFixture.file.text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import com.intellij.psi.PsiErrorElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty

class HoistStateFixBehaviorTest : BasePlatformTestCase() {
Expand Down Expand Up @@ -91,8 +90,12 @@ class HoistStateFixBehaviorTest : BasePlatformTestCase() {
val fix = HoistStateFix(propertyName)
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
property.nameIdentifier ?: property, "test", arrayOf(fix),
ProblemHighlightType.WARNING, true, false,
property.nameIdentifier ?: property,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) {
fix.applyFix(project, descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ class MatchDefaultsVisibilityFixBehaviorTest : BasePlatformTestCase() {
private fun apply(fix: LocalQuickFix, target: PsiElement): String {
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
target, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
target,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) { fix.applyFix(project, descriptor) }
return myFixture.file.text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ class MiscQuickFixBehaviorTest : BasePlatformTestCase() {
private fun apply(fix: LocalQuickFix, target: PsiElement): String {
val manager = InspectionManager.getInstance(project)
val descriptor = manager.createProblemDescriptor(
target, "test", arrayOf(fix), ProblemHighlightType.WARNING, true, false,
target,
"test",
arrayOf(fix),
ProblemHighlightType.WARNING,
true,
false,
)
WriteCommandAction.runWriteCommandAction(project) { fix.applyFix(project, descriptor) }
return myFixture.file.text
Expand Down
Loading