ktrs_compose/rules/
lambda_parameter_in_restartable_effect.rs1use 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 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(¶meter_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
80fn 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.";