Skip to main content

ktrs_compose/rules/
modifier_not_used_at_root.rs

1//! Port of `rules/ModifierNotUsedAtRoot.kt`.
2
3use ktrs_ast::psi::{KtCallExpression, KtDotQualifiedExpression, KtFunction, KtReferenceExpression, KtValueArgument};
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::{emits_content, is_in_content_emitters_denylist};
10use crate::core::util::kt_call_expressions::is_fully_shadowed;
11use crate::core::util::kt_dot_qualified_expressions::root_expression;
12use crate::core::util::modifiers::{
13    arguments_using_modifiers, modifier_parameter, modifier_type_names, obtain_all_modifier_names, shadowing_names,
14};
15use crate::core::util::psi_elements::find_all_children;
16use crate::rules::modifier_clickable_order::callee_starts_upper_case;
17
18pub struct ModifierNotUsedAtRoot;
19
20impl ComposeKtVisitor for ModifierNotUsedAtRoot {
21    fn visit_composable(&self, ast: &mut Ast, function: KtFunction, emitter: &mut dyn Emitter, config: &dyn ComposeKtConfig) {
22        let Some(modifier) = modifier_parameter(ast, function, config) else { return };
23        if modifier.name(ast).as_deref() != Some("modifier") {
24            return;
25        }
26        let Some(code) = function.body_block_expression(ast) else { return };
27        let modifiers = obtain_all_modifier_names(ast, code, "modifier");
28        let type_names = modifier_type_names(config);
29        let errors: Vec<KtValueArgument> = find_all_children::<KtCallExpression>(ast, code.node())
30            .into_iter()
31            .filter(|it| callee_starts_upper_case(ast, *it))
32            .filter_map(|call_expression| {
33                let args = arguments_using_modifiers(ast, call_expression, &modifiers, &type_names);
34                let first = *args.first()?;
35                let shadowed_names = shadowed_modifier_names_up_to(ast, call_expression, function.node(), &modifiers);
36                let usage = args
37                    .iter()
38                    .copied()
39                    .find(|arg| {
40                        let Some(expr) = arg.argument_expression(ast) else { return true };
41                        if KtReferenceExpression::is(ast, expr) {
42                            !shadowed_names.contains(&ast.text(expr))
43                        } else if let Some(dot) = KtDotQualifiedExpression::cast(ast, expr) {
44                            !shadowed_names.contains(&ast.text(root_expression(ast, dot)))
45                        } else {
46                            true
47                        }
48                    })
49                    .unwrap_or(first);
50                Some((call_expression, usage))
51            })
52            .filter(|(call_expression, _)| !is_fully_shadowed(ast, *call_expression, &modifiers, function.node(), &type_names))
53            .filter(|(call_expression, _)| {
54                ast.parents(call_expression.node())
55                    .take_while(|&it| it != code.node())
56                    .filter_map(|it| KtCallExpression::cast(ast, it))
57                    .take_while(|it| !is_in_content_emitters_denylist(ast, *it, config))
58                    .any(|it| emits_content(ast, it, config))
59            })
60            .map(|(_, usage)| usage)
61            .collect();
62        for value_argument in errors {
63            emitter.report(ast, value_argument.node(), COMPOSABLE_MODIFIER_SHOULD_BE_USED_AT_THE_TOP_MOST_POSSIBLE_PLACE, false);
64        }
65    }
66}
67
68/// `KtCallExpression.shadowedModifierNamesUpTo(stopAt, modifiers)`.
69fn shadowed_modifier_names_up_to(ast: &Ast, call: KtCallExpression, stop_at: NodeId, modifiers: &[String]) -> Vec<String> {
70    let mut set: Vec<String> = Vec::new();
71    for func in ast.parents(call.node()).take_while(|&it| it != stop_at).filter_map(|it| KtFunction::cast(ast, it)) {
72        for name in func.value_parameters(ast).into_iter().flat_map(|param| shadowing_names(ast, param, modifiers)) {
73            if !set.contains(&name) {
74                set.push(name);
75            }
76        }
77    }
78    set
79}
80
81pub const COMPOSABLE_MODIFIER_SHOULD_BE_USED_AT_THE_TOP_MOST_POSSIBLE_PLACE: &str = "\
82The main Modifier of a @Composable should be applied once as a first modifier in the chain to the root-most layout in the component implementation.
83You should move the modifier usage to the appropriate parent Composable.
84See https://mrmans0n.github.io/compose-rules/rules/#modifiers-should-be-used-at-the-top-most-layout-of-the-component for more information.";