Skip to main content

ktrs_compose/rules/
modifier_reused.rs

1//! Port of `rules/ModifierReused.kt`.
2
3use 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
17/// The context `modifierUsagesSet()` (a local fun upstream) closes over.
18struct 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    /// `Sequence<PsiElement>.modifierUsagesSet()`.
32    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.";