ktrs_compose/rules/
modifier_not_used_at_root.rs1use 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
68fn 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.";