ktrs_compose/rules/
defaults_visibility.rs1use ktrs_ast::psi::{KtClassOrObject, KtFunction, KtModifierListOwner, KtReferenceExpression, containing_kt_file};
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::kt_annotateds::is_composable;
10use crate::core::util::kt_functions::{is_internal, is_private, is_protected};
11use crate::core::util::psi_elements::find_all_children;
12
13pub struct DefaultsVisibility;
14
15impl ComposeKtVisitor for DefaultsVisibility {
16 fn visit_class_or_object(&self, ast: &mut Ast, clazz: KtClassOrObject, emitter: &mut dyn Emitter, _config: &dyn ComposeKtConfig) {
17 let Some(default_object_name) = clazz.name(ast) else { return };
18 let Some(composable_name) = default_object_name.strip_suffix("Defaults") else { return };
19 if composable_name.is_empty() {
20 return;
21 }
22 let file = containing_kt_file(ast, clazz.node()).expect("containingKtFile");
23 let references_defaults = |ast: &Ast, root: NodeId| {
24 find_all_children::<KtReferenceExpression>(ast, root).into_iter().any(|it| it.text(ast) == default_object_name)
25 };
26 let most_visible_composable = find_all_children::<KtFunction>(ast, file.node())
27 .into_iter()
28 .filter(|it| is_composable(ast, it.node()))
29 .filter(|it| it.name(ast).as_deref() == Some(composable_name))
30 .filter(|composable| {
31 let has_reference_in_parameters = composable
32 .value_parameters(ast)
33 .into_iter()
34 .filter_map(|it| it.default_value(ast))
35 .any(|it| references_defaults(ast, it));
36 if has_reference_in_parameters {
37 return true;
38 }
39 composable.body_block_expression(ast).is_some_and(|body| references_defaults(ast, body.node()))
40 })
41 .fold(None::<KtFunction>, |best, it| match best {
42 Some(b) if visibility_int(ast, b.node()) >= visibility_int(ast, it.node()) => Some(b),
43 _ => Some(it),
44 });
45 let Some(most_visible_composable) = most_visible_composable else { return };
46 if visibility_int(ast, clazz.node()) < visibility_int(ast, most_visible_composable.node()) {
47 let message = create_message(
48 visibility_string(ast, most_visible_composable.node()),
49 &default_object_name,
50 visibility_string(ast, clazz.node()),
51 );
52 emitter.report(ast, clazz.node(), &message, false);
53 }
54 }
55}
56
57fn visibility_string(ast: &Ast, owner: NodeId) -> &'static str {
58 if KtModifierListOwner::of(ast, owner).is_public(ast) {
59 "public"
60 } else if is_protected(ast, owner) {
61 "protected"
62 } else if is_internal(ast, owner) {
63 "internal"
64 } else if is_private(ast, owner) {
65 "private"
66 } else {
67 "not supported"
68 }
69}
70
71fn visibility_int(ast: &Ast, owner: NodeId) -> i32 {
72 if KtModifierListOwner::of(ast, owner).is_public(ast) {
73 4
74 } else if is_internal(ast, owner) {
75 3
76 } else if is_protected(ast, owner) {
77 2
78 } else if is_private(ast, owner) {
79 1
80 } else {
81 0
82 }
83}
84
85pub fn create_message(composable_visibility: &str, default_object_name: &str, default_object_visibility: &str) -> String {
86 format!(
87 "`Defaults` objects should match visibility of the composables they serve. `{default_object_name}` is \
88 {default_object_visibility} but it should be {composable_visibility}.\n\
89 See https://mrmans0n.github.io/compose-rules/rules/#componentdefaults-object-should-match-the-composable-visibility for more information."
90 )
91}