Skip to main content

ktrs_compose/rules/
lambda_parameter_in_restartable_effect.rs

1//! Port of `rules/LambdaParameterInRestartableEffect.kt`.
2
3use ktrs_ast::Ast;
4use ktrs_ast::psi::{KtCallExpression, KtFile, KtFunction, KtIfExpression, KtParameter, KtReferenceExpression, is_dot_selector};
5
6use crate::core::compose_kt_config::ComposeKtConfig;
7use crate::core::compose_kt_visitor::ComposeKtVisitor;
8use crate::core::emitter::Emitter;
9use crate::core::util::composables::is_restartable_effect;
10use crate::core::util::kt_annotateds::is_composable;
11use crate::core::util::kt_call_expressions::find_shadowing_redeclarations;
12use crate::core::util::lambdas::{is_lambda, lambda_types};
13use crate::core::util::psi_elements::find_all_children;
14
15pub struct LambdaParameterInRestartableEffect;
16
17impl ComposeKtVisitor for LambdaParameterInRestartableEffect {
18    fn visit_file(&self, ast: &mut Ast, file: KtFile, emitter: &mut dyn Emitter, config: &dyn ComposeKtConfig) {
19        let lambda_types = lambda_types(ast, file.node(), config);
20        let composables: Vec<_> =
21            find_all_children::<KtFunction>(ast, file.node()).into_iter().filter(|it| is_composable(ast, it.node())).collect();
22        for composable in composables {
23            let effects: Vec<KtCallExpression> = find_all_children::<KtCallExpression>(ast, composable.node())
24                .into_iter()
25                .filter(|it| is_restartable_effect(ast, *it))
26                .collect();
27            if effects.is_empty() {
28                continue;
29            }
30            // `associateBy { it.name!! }`: a later duplicate replaces the value, the key keeps its first position.
31            let mut lambda_parameters: Vec<(String, KtParameter)> = Vec::new();
32            for parameter in composable.value_parameters(ast) {
33                let is_lambda_like = parameter.is_lambda_parameter(ast)
34                    || parameter.type_reference(ast).is_some_and(|t| is_lambda(ast, t, &lambda_types));
35                let Some(name) = parameter.name(ast).filter(|_| is_lambda_like) else { continue };
36                match lambda_parameters.iter_mut().find(|(n, _)| *n == name) {
37                    Some(entry) => entry.1 = parameter,
38                    None => lambda_parameters.push((name, parameter)),
39                }
40            }
41            let lambda_parameter_names: Vec<String> = lambda_parameters.iter().map(|(n, _)| n.clone()).collect();
42            if lambda_parameter_names.is_empty() {
43                continue;
44            }
45            let mut used_lambda_parameter_names: Vec<String> = Vec::new();
46            for effect in &effects {
47                for name in used_in_effect(ast, *effect, &lambda_parameter_names) {
48                    if !used_lambda_parameter_names.contains(&name) {
49                        used_lambda_parameter_names.push(name);
50                    }
51                }
52            }
53            let keyed_lambda_parameter_names: Vec<String> = effects
54                .iter()
55                .flat_map(|it| it.value_arguments(ast))
56                .filter_map(|it| it.argument_expression(ast))
57                .filter(|&it| KtReferenceExpression::is(ast, it))
58                .map(|it| ast.text(it))
59                .filter(|it| used_lambda_parameter_names.contains(it))
60                .collect();
61            let shadowed_parameters: Vec<&String> = used_lambda_parameter_names
62                .iter()
63                .filter(|it| {
64                    effects
65                        .iter()
66                        .any(|effect| !find_shadowing_redeclarations(ast, *effect, it, composable.node()).is_empty())
67                })
68                .collect();
69            for parameter_name in &used_lambda_parameter_names {
70                if keyed_lambda_parameter_names.contains(parameter_name) || shadowed_parameters.contains(&parameter_name) {
71                    continue;
72                }
73                let parameter = lambda_parameters.iter().find(|(n, _)| n == parameter_name).unwrap().1;
74                emitter.report(ast, parameter.node(), LAMBDA_USED_IN_RESTARTABLE_EFFECT, false);
75            }
76        }
77    }
78}
79
80/// The `flatMap { effect -> }` body: the lambda parameters invoked or forwarded in the effect's trailing lambda.
81fn used_in_effect(ast: &Ast, effect: KtCallExpression, lambda_parameter_names: &[String]) -> Vec<String> {
82    let Some(body) = effect.lambda_arguments(ast).last().and_then(|it| it.lambda_expression(ast)).and_then(|it| it.body_expression(ast))
83    else {
84        return Vec::new();
85    };
86    let call_expressions: Vec<KtCallExpression> = find_all_children::<KtCallExpression>(ast, body.node())
87        .into_iter()
88        .filter(|it| !is_dot_selector(ast, it.node()))
89        .collect();
90    let callee_text = |call: KtCallExpression| call.callee_expression(ast).map(|c| ast.text(c));
91    let effect_callee = callee_text(effect);
92    let is_disposable_effect = effect_callee.as_deref() == Some("DisposableEffect");
93    let is_lifecycle_effect = matches!(effect_callee.as_deref(), Some("LifecycleStartEffect" | "LifecycleResumeEffect"));
94    let mut used: Vec<String> = call_expressions
95        .iter()
96        .filter(|it| {
97            let callee = callee_text(**it);
98            if is_disposable_effect {
99                callee.as_deref() != Some("onDispose")
100            } else if is_lifecycle_effect {
101                !callee.is_some_and(|c| LIFECYCLE_EFFECT_SCOPE_FUNCTIONS.contains(&c.as_str()))
102            } else {
103                true
104            }
105        })
106        .filter_map(|it| callee_text(*it))
107        .filter(|it| lambda_parameter_names.contains(it))
108        .collect();
109    let in_names = |text: Option<String>| text.filter(|t| lambda_parameter_names.contains(t));
110    for call in &call_expressions {
111        for argument in call.value_arguments(ast) {
112            let Some(expression) = argument.argument_expression(ast) else { continue };
113            let forwarded = if KtReferenceExpression::is(ast, expression) {
114                in_names(Some(ast.text(expression)))
115            } else if let Some(if_expression) = KtIfExpression::cast(ast, expression) {
116                in_names(if_expression.then(ast).map(|t| ast.text(t)))
117                    .or_else(|| in_names(if_expression.r#else(ast).map(|e| ast.text(e))))
118            } else {
119                None
120            };
121            used.extend(forwarded);
122        }
123    }
124    used
125}
126
127const LIFECYCLE_EFFECT_SCOPE_FUNCTIONS: &[&str] = &["onStopOrDispose", "onPauseOrDispose"];
128
129pub const LAMBDA_USED_IN_RESTARTABLE_EFFECT: &str = "\
130Lambda parameters in a @Composable that are referenced directly inside of restarting effects can cause issues or unpredictable behavior.
131If restarting the effect is ok, you can add the reference to this parameter as a key in that effect, so when the parameter changes, a new effect is created.
132However, if the effect is not to be restarted, you will need to use `rememberUpdatedState` on the parameter and use its result in the effect.
133See https://mrmans0n.github.io/compose-rules/rules/#be-mindful-of-the-arguments-you-use-inside-of-a-restarting-effect for more information.";