Skip to content
rinekri edited this page Oct 10, 2026 · 13 revisions

Rules of detekt-rules-compose, described as of version 2.2.0.

Rule Active by default Needs type resolution
ReusedModifierInstance yes yes
UnnecessaryEventHandlerParameter yes yes
ComposableEventParameterNaming no no
ComposableParametersOrdering yes no
ModifierDefaultValue yes no
MissingModifierDefaultValue yes no
ModifierHeightWithText yes no
PublicComposablePreview yes no
TopLevelComposableFunctions no no
ComposableFunctionName yes no
ConditionCouldBeLifted no yes
UnnecessaryLayoutWrapper no no
StateReadCouldBeLowered no yes
NonImmutableDataClassParameter no yes
PartiallyUsedViewState no yes
UnwrappedValueClassImageModel no yes
UnnecessaryModifierArgument no yes

"Active by default" is the state in the configuration bundled with the rule set; every rule can be turned on or off in your detekt-config.yml. Rules which need type resolution only run when detekt has it (detekt 1, --analysis-mode full on detekt 2). Without it such a rule reports nothing. All five rules added in 2.2.0 need type resolution.

The rule ids, options and behaviour are the same in both artifacts, ru.kode:detekt-rules-compose (detekt 1) and ru.kode:detekt-rules-compose-detekt2 (detekt 2).

Rule ReusedModifierInstance

Active by default. Needs type resolution.

Finds usages of the modifier parameter on non-top-level children of a composable function. This tends to happen during refactorings and often leads to incorrect rendering of a composable.

For example, imagine that Column here used to be a top composable, but then it got wrapped by the Row. But the modifier parameter was moved along with it and is now applied to the wrong composable:

@Composable
fun MyComposable(modifier: Modifier) {
  Row(modifier = Modifier.padding(30.dp)) {
    Column(modifier = modifier.padding(20.dp)) {
    }
  }
}

@Composable
fun Content() {
  MyComposable(modifier = Modifier.background(color = Color.Green))
}

This should be fixed by using modifier parameter on the Row instead:

@Composable
fun MyComposable(modifier: Modifier) {
  Row(modifier = modifier.height(30.dp)) {
    Column(modifier = Modifier.padding(20.dp)) {
    }
  }
}

A name which only shadows the parameter (a lambda or local function parameter, a local val not derived from the parameter, a loop variable and so on) is not mistaken for it.

Rule UnnecessaryEventHandlerParameter

Active by default. Needs type resolution.

Suggests hoisting event argument passing to the upper level which often simplifies individual composable components. This makes individual components less coupled to the structure of their parameters and leaves that to the parent, which in turn often leads to simplification of a composable.

For example here the PrettyButton is unnecessary coupled to the structure of Data — it extracts id field inside the onClick:

data class Data(id: Int, title: String)

fun PrettyButton(data: Data, onAction: (Int) -> Unit) {
  Button(onClick = { onAction(data.id) })
}

fun Parent() {
  val data = Data(id = 3, title = "foo")
  PrettyButton(data = data, onAction = { id -> process(id) })
}

This "knowledge" of id can be moved to the parent which would not only simplify the PrettyButton by removing unnecessary lambda wrapper around onAction call, but this also makes it easier to work with PrettyButton later, during refactorings. Here the data.id is hoisted into the parent:

fun PrettyButton(data: Data, onAction: () -> Unit) {
  Button(onClick = onAction)
}

fun Parent() {
  val data = Data(id = 3, title = "foo")
  PrettyButton(data = data, onAction = { process(data.id) })
}

Constant arguments

A constant argument is reported for the same reason, the parent already knows it. A constant is a literal, a string without templates, a Kotlin const val, an enum entry or an object:

fun Toolbar(onUserIntent: (Intent) -> Unit) {
  IconButton(onClick = { onUserIntent(Intent.Back) }) // ❌ should be onBack: () -> Unit
}

Set reportConstantArguments: false to turn this part off.

When an argument is reported

An argument is reported only when the parent could really supply it:

  • every call of the handler (onAction(...), onAction.invoke(...) or onAction?.invoke(...)) passes the same expression at that position. When the calls disagree, the argument is what tells them apart, and nothing is reported:

    fun Item(id: String, position: Int, visible: Boolean, onShown: (Any, Int) -> Unit) {
      onShown(id, position)
      onShown(visible, 0) // ✅ different arguments, not reported
    }
    
    fun Switch(onCheckedChange: (Boolean) -> Unit) {
      onCheckedChange(true)
      onCheckedChange(false) // ✅ different constants, not reported
    }
  • the handler is not used in any other way, for example passed on as a value (Child(onClick = onClick)): its type is then dictated by that other user. A comparison with null does not count as such a use.

There is one finding per handler for state arguments and one for constants, each lists everything that can be moved.

A function which only shares its name with the handler, like an extension Modifier.onShown(...) called inside a composable with an onShown parameter, is not a call of the handler and is not reported.

UnnecessaryEventHandlerParameter:
  active: true
  reportConstantArguments: true

Rule ComposableEventParameterNaming

Not active by default.

Ensures that all event handler parameters of composable functions are named in the same Compose-like style, i.e. they have on prefix and do not use past tense.

This rule suggests naming improvements

fun Button(click: () -> Unit) // ❌ wrong: missing "on"
fun Button(onClick: () -> Unit) // ✅ correct

fun Box(scroll: () -> Unit) // ❌ wrong: missing "on"
fun Box(onScroll: () -> Unit) // ✅ correct

fun Box(onScrolled: () -> Unit) // ❌ wrong: using past tense
fun Box(onScroll: () -> Unit) // ✅ correct

Rule ComposableParametersOrdering

Active by default.

Checks that parameters of Composable functions have a correct order:

  1. Required parameters come first
  2. Optional parameters come after required, the modifier parameter first among them
  3. Slots named in trailingSlotNames (content by default) come last, so they can be passed as a trailing lambda

Non-compliant:

Header(
  title: String,
  enabled: Boolean = false,
  description: String,
)

Compliant:

Header(
  title: String,
  description: String,
  enabled: Boolean = false,
)

Position of modifier

modifier must be the first parameter after the required ones and before the other optional ones (this used to be a separate rule, ModifierParameterPosition):

@Composable
fun Button(
  modifier: Modifier = Modifier,
  text: String,
  onClick: () -> Unit,
  arrangement: Arrangement = Vertical,
)

Should be replaced with:

@Composable
fun Button(
  text: String,
  onClick: () -> Unit,
  modifier: Modifier = Modifier,
  arrangement: Arrangement = Vertical,
)

Rationale: this would allow one to write a short form

Button("Continue", onClick = { ... })
Title("Hello")

otherwise it would be required to always use named parameters

Button(text = "Continue", onClick = { ... })
Title(text = "Hello")

Google's androidx.compose.material composables follow this convention.

Slots and trailing lambdas

Composable slots are not forced to the end: a required slot can stay among the required parameters and an optional one among the optional parameters, as in Material's TextField and AlertDialog. Two things are checked instead:

  • a slot named in trailingSlotNames must be the last parameter
  • after the first optional parameter only the last parameter may be a required composable slot
@Composable
fun Card(title: String, content: @Composable () -> Unit, footer: @Composable () -> Unit) // ❌ "content" should be last

@Composable
fun Card(modifier: Modifier = Modifier, icon: @Composable () -> Unit, label: @Composable () -> Unit) // ❌ "icon" is required but can't be a trailing lambda

@Composable
fun Card(
  title: String,
  icon: @Composable () -> Unit,
  modifier: Modifier = Modifier,
  footer: (@Composable () -> Unit)? = null,
  content: @Composable () -> Unit,
) // ✅

Lambdas at the end of the parameter list may follow the optional parameters. By default this includes required event handlers:

@Composable
fun Item(id: String, modifier: Modifier = Modifier, onClick: () -> Unit) // ✅ by default

With allowTrailingEventHandlers: false such a handler is reported and should be moved to the required parameters (id: String, onClick: () -> Unit, modifier: Modifier = Modifier). An event handler here is a non-composable lambda without a receiver which returns Unit. Lambdas with a receiver (content: LazyListScope.() -> Unit), lambdas returning a value (key: (Int) -> Any), handlers with a default value and parameters named in trailingSlotNames are not reported.

With allowTrailingLambdas: false the same applies to every required non-composable lambda, event handlers included: validator: (String) -> Boolean or onDraw: DrawScope.() -> Unit after optional parameters is reported too. Parameters named in trailingSlotNames are still not reported, so add the names of DSL builder lambdas which should stay last (like content: LazyListScope.() -> Unit) there.

Functions which are not checked

Overriding and actual functions are skipped: their order is dictated by the overridden or expect declaration, which is checked itself.

ComposableParametersOrdering:
  active: true
  trailingSlotNames: [ 'content' ]
  allowTrailingEventHandlers: true
  allowTrailingLambdas: true

Rule ModifierDefaultValue

Active by default.

Checks that the modifier parameter of a Composable function has the correct default value.

Using a default value other than Modifier can lead to various non-obvious issues and inconveniences.

Non-compliant:

fun Content(modifier: Modifier = Modifier.fillMaxSize()) {
  Text("Greetings", modifier) // fillMaxSize will be ignored here
}

Compliant:

fun Content(modifier: Modifier = Modifier) {
  Text("Greetings", modifier.fillMaxSize())
}

Rule MissingModifierDefaultValue

Active by default.

Checks that the modifier parameter of a Composable function has a default value.

Non-compliant:

fun Content(modifier: Modifier) {
  Text("Greetings")
}

Compliant:

fun Content(modifier: Modifier = Modifier) {
  Text("Greetings")
}

Not reported:

  • overriding and actual functions, they can't declare default values
  • the function of a fun interface, for the same reason
  • abstract and open functions and functions of interfaces, unless checkAbstractFunctions: true is set

checkAbstractFunctions is off by default because the Compose compiler accepts default values in abstract composables only from Kotlin language version 2.1 and in open ones from 2.2 (K2 compiler); older setups fail to compile such code. Turn it on if your project is past these versions:

interface Screen {
  @Composable
  fun Content(modifier: Modifier) // ❌ with checkAbstractFunctions: true
}
MissingModifierDefaultValue:
  active: true
  checkAbstractFunctions: false

Rule ModifierHeightWithText

Active by default.

Suggests using Modifier.heightIn() instead of Modifier.height() on a layouts which have Text children, so that if the text turns out to be long and would overflow and wrap, layout will not cut it off

Row(modifier = Modifier.height(24.dp)) {
  Text("hello")
}

with

Row(modifier = Modifier.heightIn(min = 24.dp)) {
  Text("hello")
}

height(IntrinsicSize.Min) and height(IntrinsicSize.Max) size the layout to its content and can't clip the text, so they are not reported.

Rule PublicComposablePreview

Active by default.

Finds and reports composable previews which are not marked as private

Rule TopLevelComposableFunctions

Not active by default.

Checks that composable function is defined as a top-level function.

Non-compliant code would look like this:

interface Screen {
  @Composable
  fun Content(modifier: Modifier = Modifier)
}

class ScreenImpl : Screen {
  @Composable
  override fun Content(modifier: Modifier) {
    Text("Greetings", modifier.fillMaxSize())
  }
}

And the compliant code is to have a top-level composable function:

fun ScreenContent(modifier: Modifier = Modifier) {
  Text("Greetings", modifier.fillMaxSize())
}

Two options relax the rule:

  • allowInObjects: true permits composable functions in objects
  • allowInInterfaces: true permits composable functions declared in interfaces (for example a composable provided by another module through DI)

Composable functions in classes are always reported.

TopLevelComposableFunctions:
  active: true
  allowInObjects: false
  allowInInterfaces: false

Rule ComposableFunctionName

Active by default.

The @Composable functions that return Unit should start with upper-case while the ones that return a value should start with lower case.

Examples of compliant and non-compliant code are given below.

Non-compliant:

@Composable
fun button() {
  …
}

Correct:

@Composable
fun Button() {
  …
}

Non-compliant:

@Composable
fun Value(): Int = …

Compliant:

@Composable
fun value(): Int = …

See also: Compose API guidelines.

Rule ConditionCouldBeLifted

Not active by default. Needs type resolution.

Reports cases where a Compose layout contains a single conditional expression which could be "lifted" out of it.

For example in this code the if-condition could be moved outside of Column without any problems and this could potentially result in snappier rendering due to less composables having to be accounted for and/or recomposed:

Column {
  if(condition) {
    Row()
    Row()
  }
}

When moved out:

if(condition) {
  Column {
    Row()
    Row()
  }
}

Not reported, because the condition can't be lifted without changing what is shown:

  • a condition on a name declared inside the content lambda: its parameters (including the implicit it) or a local val declared before the if

    Crossfade(targetState = expanded) { isExpanded ->
      if (isExpanded) { Details() } // ✅ isExpanded exists only inside the lambda
    }
  • a layout which also has a composable call outside the if, in any form: slot?.invoke(), slot?.let { it() }, items.forEach { Text(it) }, repeat(n) { Text() }, when (x) { a -> Text() }

Use ignoreCallsWithArgumentNames config option to specify argument names which (when present) will make this rule ignore and skip those calls:

// in detekt-config.yaml
ConditionCouldBeLifted:
  active: true
  ignoreCallsWithArgumentNames: [ 'modifier', 'contentAlignment' ]

The bundled default is [ 'modifier' ].

Rule UnnecessaryLayoutWrapper

Not active by default.

Reports a Box, Column or Row which has no parameters and only wraps a single Box, Column or Row: such a wrapper usually adds nothing but a layout node, and its child can be used directly.

Non-compliant:

Box {
  Row(modifier = Modifier.padding(16.dp)) {
    Text("hello")
  }
}

Compliant:

Row(modifier = Modifier.padding(16.dp)) {
  Text("hello")
}

The check is name-based and conservative: the wrapper must have nothing but a trailing content lambda, and the lambda must contain nothing but the child call. Still, removing a wrapper is not always a no-op, which is why the rule is not active by default:

  • scope modifiers: a child using weight, align, alignBy, alignByBaseline or matchParentSize depends on the wrapper's scope and is not reported, but the same modifiers passed through a variable are not detected
  • min constraints: the wrapper measures its child with a minimum size of 0; without it the child gets the incoming minimum constraints (for example as the content of a Surface) and can be stretched
  • name-based matching: project components named Box, Row or Column are matched too

Rule StateReadCouldBeLowered

Not active by default. Needs type resolution.

Reports a val of a composable function which holds a state read (.value of a State or .current of a CompositionLocal) and is only used minDepth or more composable lambdas deeper. A state read in the function body makes the whole function recompose when the state changes; read next to its use, it recomposes only the scope which needs it.

Non-compliant:

@Composable
fun Screen() {
  val isPortrait = LocalOrientation.current == Orientation.Portrait
  Card {
    Surface {
      if (isPortrait) Text("Portrait")
    }
  }
}

Compliant:

@Composable
fun Screen() {
  Card {
    Surface {
      val isPortrait = LocalOrientation.current == Orientation.Portrait
      if (isPortrait) Text("Portrait")
    }
  }
}

How the depth is counted:

  • a lambda passed to any composable lambda parameter counts, not only content. The message names the deepest lambda shared by all usages: "count" is only used inside the content of "Surface", or, for another slot, "title" is only used inside the "topBar" slot of "Scaffold"

  • the lambda of an inline composable (Box, Column, Row) does not count: it is inlined into the caller and is no recomposition scope of its own, a read moved into it is still a read of the scope around

    val count = state.value
    Card {
      Column { // inline, not a level
        Text(text = count.toString()) // ✅ one level deep only
      }
    }
  • a crossinline lambda (itemContent of the lazy list items(list) { }) and a noinline lambda of an inline composable do count: they end up in a lambda of their own

    val count = state.value
    LazyColumn {
      items(list) { Text(text = (it + count).toString()) } // ❌ used inside the "itemContent" slot of "items"
    }
  • a lambda which is not composable (onClick, forEach { }) does not count

Not reported:

  • .value of a receiver which is not a State and .current of one which is not a CompositionLocal (subtypes like MutableState, nullable and smart cast receivers are recognized)
  • a delegate (val count by state), a var, a val with a remember* call or a lambda in its initializer
  • a val whose initializer uses a name which is redeclared on the way down: moved there, it would read another declaration
  • a val which is not used, is used in the function body or fewer than minDepth lambdas deep

Limits: only the vals declared directly in the block body of a composable function are checked, the ones in nested blocks and functions with an expression body are not.

// in detekt-config.yaml
StateReadCouldBeLowered:
  active: true
  minDepth: 2

The bundled default of minDepth is 2.

Rule NonImmutableDataClassParameter

Not active by default. Needs type resolution.

Reports a parameter of a composable function whose type, or one of its type arguments, is a data class which is not marked with @Immutable or @Stable.

With strong skipping (the default since Kotlin 2.0.20) a composable with an unstable parameter is skippable too, but such a parameter is compared by instance (===) instead of equals(): an equal data class which is created anew, by a copy() or a mapper, still recomposes the composable. The annotation brings the comparison by equals() back.

Non-compliant:

data class Profile(val name: String)

@Composable
fun ProfileCard(profile: Profile) {} // ❌ is a data class "Profile" which is not marked with @Immutable or @Stable

@Composable
fun ProfileList(profiles: List<Profile>) {} // ❌ uses a data class "Profile" which is not marked …

Compliant:

@Immutable
data class Profile(val name: String)

@Composable
fun ProfileCard(profile: Profile) {}

Not reported:

  • data classes of the kotlin package (Pair, Triple), they can't be annotated
  • data classes of a library built with the Compose compiler which are known to be stable at compile time (@StabilityInferred). A generic one is stable together with the type arguments it depends on, and only those arguments are checked: with data class Box<T>(val value: T) in a library, Box<Int> is not reported and Box<Profile> reports Profile
  • data classes of the packages listed in ignoredPackageSegments, for example generated ones. A segment is matched against the package only, not against the names of outer classes
  • data classes matching stableTypePatterns
  • vararg parameters, types which are not data classes, types which can't be resolved

stableTypePatterns has the syntax of the stability configuration file of the Compose compiler. A detekt rule can't locate that file (stability_config.conf) itself, copy its lines into the option:

  • * is one or more characters within one name segment: java.time.*, com.example.P*
  • ** crosses the segments: com.example.**, com.**.Inner
  • a generic suffix tells which type arguments are still checked: in com.example.Both<*,_> the first argument is checked and the second (_) is not. Without the suffix all arguments are checked
  • blank lines and lines starting with // are skipped
  • a line which the Compose compiler would not accept (com.example.*Profile, a trailing comment, com/example/Profile) fails the run with NonImmutableDataClassParameter: stableTypePatterns "<pattern>" is not a valid pattern

Limits:

  • a class of the linted module is judged by its annotations only. The stability which the Compose compiler would infer for it (all properties are vals of stable types) is not computed, the class has no @StabilityInferred inside detekt. Annotate such a class or list it in stableTypePatterns
  • a library data class whose stability the Compose compiler left to the runtime (it has properties of classes of another module) is reported. List it in stableTypePatterns
  • unlike the Compose compiler, the rule does not match a pattern against the supertypes of a class
  • the type arguments are read from the written type, the ones behind a type alias are not seen
  • only data classes are judged: an unstable List, java.util.Date or a class with a var is not reported, neither is the extension receiver of the composable. List<Profile> stays unstable for the Compose compiler even with an annotated Profile, use an immutable collection or the stability configuration file for that

The ImmutableDataClass rule of the kode rule set reports the declaration of a data class in a ui package without @Immutable. This rule reports the usage, wherever the class is declared, and accepts @Stable too. When both are active, ignoredPackageSegments: [ 'ui' ] leaves the classes of the ui packages to ImmutableDataClass.

// in detekt-config.yaml
NonImmutableDataClassParameter:
  active: true
  ignoredPackageSegments: [ 'schema' ]
  stableTypePatterns: [ 'com.example.model.**', 'java.time.*' ]

Both options are empty by default.

Rule PartiallyUsedViewState

Not active by default. Needs type resolution.

Reports a composable function which takes a whole view state but reads only a few of its properties. Such a function is recomposed on every change of the state, it should take the properties it needs instead.

Non-compliant:

data class ViewState(val title: String, val subtitle: String, val items: List<Item>, val isLoading: Boolean)

@Composable
fun Header(state: ViewState) { // ❌ reads only 1 of 4 "ViewState" properties (title)
  Text(text = state.title)
}

Compliant:

@Composable
fun Header(title: String) {
  Text(text = title)
}

A parameter is checked when the simple name of its class matches the regex stateClassPattern and the class has at least usedFieldsThreshold properties. It is reported when at least one and fewer than usedFieldsThreshold of them are read. A nullable state (state: ViewState?, state?.title, state!!.title, state != null) is handled the same way.

What counts:

  • only public and internal properties, the inherited ones included; private and extension properties are neither counted nor reads
  • state.title
  • title inside with(state) { }, state.run { } and state.apply { }. The read is matched by the receiver instance, so inside nested scope functions a name belongs to the receiver it is resolved on
  • a destructuring declaration: val (title, _) = state reads title

Not reported:

  • a function which uses the state as a whole: passes it on (log(state), remember(state) { }), calls its function (state.copy(), describe() inside with(state) { }), mentions this inside the scope function, or uses the result of state.apply { } as a value, also as the value of an if, when or try branch
  • state.let { it.title } and state.also { }: they pass the state on, so they are uses of the whole state
  • a function whose signature is not its own: override, open, abstract, expect, actual or declared in an interface
  • a state which is not read at all or is only compared with null
  • vararg parameters and functions which are not composable

An invalid stateClassPattern fails the run with PartiallyUsedViewState: stateClassPattern "<pattern>" is not a valid regex.

Limits: with, run and apply are matched by name.

// in detekt-config.yaml
PartiallyUsedViewState:
  active: true
  stateClassPattern: '.*ViewState'
  usedFieldsThreshold: 3

The bundled defaults are '.*ViewState' and 3.

Rule UnwrappedValueClassImageModel

Not active by default. Needs type resolution.

Reports a value class passed as is as a Coil image model. The model parameter is Any?, so the code compiles, but the value class gets boxed and Coil can't load an image from it. Pass the underlying value.

Non-compliant:

@JvmInline
value class ImageUrl(val value: String)

@Composable
fun Avatar(url: ImageUrl) {
  AsyncImage(model = url, contentDescription = null)
  AsyncImage(model = ImageRequest.Builder(context).data(url).build(), contentDescription = null)
}

Compliant:

@Composable
fun Avatar(url: ImageUrl) {
  AsyncImage(model = url.value, contentDescription = null)
  AsyncImage(model = ImageRequest.Builder(context).data(url.value).build(), contentDescription = null)
}

Checked arguments:

  • the model argument (named or positional) of the functions listed in imageFunctions, matched by the name of the function
  • the data argument of a data(...) call whose resolved declaring class ends with .ImageRequest.Builder, so both coil.request.ImageRequest.Builder and the one of coil3, also when the builder is not the start of a call chain (builder.apply { data(url) })

A nullable value class (ImageUrl?) and a model which is smart cast to a value class (if (model is ImageUrl) AsyncImage(model = model, …)) are reported too.

Not reported:

  • value classes of the kotlin package (UInt, Result)
  • an unwrapped value (url.value), a regular class, Any?, an unresolved expression
  • a value class passed to other functions or other parameters
// in detekt-config.yaml
UnwrappedValueClassImageModel:
  active: true
  imageFunctions: [ 'AsyncImage', 'SubcomposeAsyncImage', 'rememberAsyncImagePainter' ]

This list is the bundled default of imageFunctions; add the names of your own image wrappers which take a model to it.

Rule UnnecessaryModifierArgument

Not active by default. Needs type resolution.

Reports a Modifier passed to a parameter which already has Modifier as its default value. The argument changes nothing and can be removed.

Non-compliant:

@Composable
fun Header(title: String) {
  Text(title, modifier = Modifier)
  AppCard(Modifier) { Text(title) }
}

Compliant:

@Composable
fun Header(title: String) {
  Text(title)
  AppCard { Text(title) }
  Text(title, modifier = Modifier.padding(8.dp))
}

Checked arguments:

  • an argument which is Modifier itself, also fully qualified (androidx.compose.ui.Modifier) or in parentheses, whether positional, named or out of order
  • passed to a parameter whose default value is Modifier itself. The name of the parameter does not matter, contentModifier: Modifier = Modifier is checked the same; neither does the kind of the function: composables, regular functions, members (inherited and delegated ones included), extensions and constructors called in an expression (AppCard(Modifier), also through a type alias) are checked
  • of the overload which is actually called

Not reported:

  • a parameter without a default value, or with another one (modifier: Modifier = Modifier.fillMaxWidth())
  • another argument: a modifier chain (Modifier.padding(8.dp)), a variable or a parameter which holds a modifier
  • a function of a library. Only the functions declared in the analyzed sources have their default values available, so Text(title, modifier = Modifier) is reported for your own Text wrapper and not for the one of Compose
  • a generated function, like the copy of a data class
  • the constructor call of a supertype (class Child : Base(Modifier)) or of an enum entry
  • a call which can't be resolved, or which is resolved with an error (a wrong argument type, an extra argument)

A positional argument followed by another positional one can't be removed alone, the next argument would move to its parameter. The message tells to pass the following arguments by name then: AppCard("Title", Modifier, false) becomes AppCard("Title", enabled = false). A trailing lambda outside the parentheses does not count, AppCard("Title", Modifier) { } only needs the argument removed, and so does an argument followed by named ones.

Removing the argument can make the call resolve to another overload which has no such parameter. The rule does not look for one, suppress the finding with @Suppress("UnnecessaryModifierArgument") in that case.

// in detekt-config.yaml
UnnecessaryModifierArgument:
  active: true

The rule has no options.