ktrs_compose/rules/
modifier_reused.rs1use ktrs_ast::psi::{KtBlockExpression, KtCallExpression, KtFunction, KtReturnExpression};
4use ktrs_ast::{Ast, NodeId};
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::function_emits_content;
10use crate::core::util::kt_call_expressions::is_fully_shadowed;
11use crate::core::util::modifiers::{is_using_modifiers, modifier_parameters, modifier_type_names, obtain_all_modifier_names};
12use crate::core::util::psi_elements::{find_all_children, walk_backwards};
13use crate::rules::modifier_clickable_order::callee_starts_upper_case;
14
15pub struct ModifierReused;
16
17struct Usages<'a> {
19 ast: &'a Ast,
20 modifier_names: &'a [String],
21 function: KtFunction,
22 type_names: &'a [String],
23}
24
25impl Usages<'_> {
26 fn uses_modifiers(&self, call: KtCallExpression) -> bool {
27 is_using_modifiers(self.ast, call, self.modifier_names, self.type_names)
28 && !is_fully_shadowed(self.ast, call, self.modifier_names, self.function.node(), self.type_names)
29 }
30
31 fn modifier_usages_set(&self, elements: impl Iterator<Item = NodeId>) -> Vec<KtCallExpression> {
33 let mut set = Vec::new();
34 for call in elements.filter_map(|it| KtCallExpression::cast(self.ast, it)).filter(|&it| self.uses_modifiers(it)) {
35 if !set.contains(&call) {
36 set.push(call);
37 }
38 }
39 set
40 }
41
42 fn hits(&self, call_expression: KtCallExpression, composable_block_expression: KtBlockExpression) -> Vec<KtCallExpression> {
43 let ast = self.ast;
44 let composable_hits = self.modifier_usages_set(
45 walk_backwards(ast, call_expression.node(), Some(composable_block_expression.node())).into_iter(),
46 );
47 if composable_hits.len() != 1 {
48 return composable_hits;
49 }
50 let prev_local_hits = self.modifier_usages_set(ast.siblings_with_itself(call_expression.node(), false, true));
51 let same = prev_local_hits.len() == composable_hits.len() && prev_local_hits.iter().all(|it| composable_hits.contains(it));
52 if !same {
53 return composable_hits;
54 }
55 let is_followed_by_early_return = ast
56 .siblings_with_itself(call_expression.node(), true, true)
57 .filter_map(|it| KtReturnExpression::cast(ast, it))
58 .any(|it| it.labeled_expression(ast).is_none());
59 if is_followed_by_early_return { Vec::new() } else { composable_hits }
60 }
61}
62
63impl ComposeKtVisitor for ModifierReused {
64 fn visit_composable(&self, ast: &mut Ast, function: KtFunction, emitter: &mut dyn Emitter, config: &dyn ComposeKtConfig) {
65 if !function_emits_content(ast, function, config) {
66 return;
67 }
68 let Some(composable_block_expression) = function.body_block_expression(ast) else { return };
69 let mut initial_modifier_names: Vec<String> = Vec::new();
70 for name in modifier_parameters(ast, function, config).into_iter().filter_map(|it| it.name(ast)) {
71 if !initial_modifier_names.contains(&name) {
72 initial_modifier_names.push(name);
73 }
74 }
75 if initial_modifier_names.is_empty() {
76 return;
77 }
78 let type_names = modifier_type_names(config);
79 let all_modifier_names: Vec<Vec<String>> =
80 initial_modifier_names.iter().map(|it| obtain_all_modifier_names(ast, composable_block_expression, it)).collect();
81 for modifier_names in all_modifier_names {
82 let usages = Usages { ast, modifier_names: &modifier_names, function, type_names: &type_names };
83 let mut reported: Vec<KtCallExpression> = Vec::new();
84 for call in find_all_children::<KtCallExpression>(ast, composable_block_expression.node())
85 .into_iter()
86 .filter(|it| callee_starts_upper_case(ast, *it))
87 .filter(|&it| usages.uses_modifiers(it))
88 .map(|call_expression| usages.hits(call_expression, composable_block_expression))
89 .filter(|it| it.len() > 1)
90 .flatten()
91 {
92 if !reported.contains(&call) {
93 reported.push(call);
94 }
95 }
96 for call_expression in reported {
97 emitter.report(ast, call_expression.node(), MODIFIER_SHOULD_BE_USED_ONCE_ONLY, false);
98 }
99 }
100 }
101}
102
103pub const MODIFIER_SHOULD_BE_USED_ONCE_ONLY: &str = "\
104Modifiers should only be used once and by the root level layout of a Composable. This is true even if appended to or with other modifiers e.g. 'modifier.fillMaxWidth()'.
105Use Modifier (with a capital 'M') to construct a new Modifier that you can pass to other composables.
106See https://mrmans0n.github.io/compose-rules/rules/#dont-re-use-modifiers for more information.";