Skip to main content

ktrs_compose/rules/
parameter_order.rs

1//! Port of `rules/ParameterOrder.kt`.
2
3use std::cmp::Reverse;
4
5use ktrs_ast::Ast;
6use ktrs_ast::psi::{KtFile, KtFunction, KtParameter};
7
8use crate::core::compose_kt_config::ComposeKtConfig;
9use crate::core::compose_kt_visitor::ComposeKtVisitor;
10use crate::core::emitter::Emitter;
11use crate::core::util::kt_annotateds::is_composable;
12use crate::core::util::lambdas::{composable_lambda_types, is_composable_lambda, is_lambda, lambda_types};
13use crate::core::util::modifiers::is_modifier;
14use crate::core::util::psi_elements::find_all_children;
15
16pub struct ParameterOrder;
17
18impl ComposeKtVisitor for ParameterOrder {
19    fn visit_file(&self, ast: &mut Ast, file: KtFile, emitter: &mut dyn Emitter, config: &dyn ComposeKtConfig) {
20        let lambda_types = lambda_types(ast, file.node(), config);
21        let composable_lambda_types = composable_lambda_types(ast, file.node(), config);
22        let composables: Vec<_> =
23            find_all_children::<KtFunction>(ast, file.node()).into_iter().filter(|it| is_composable(ast, it.node())).collect();
24        for function in composables {
25            let current_order = function.value_parameters(ast);
26            let has_trailing_function = current_order
27                .last()
28                .and_then(|it| it.type_reference(ast))
29                .is_some_and(|t| is_lambda(ast, t, &lambda_types));
30            let trailing_lambda: Vec<KtParameter> =
31                if has_trailing_function { vec![*current_order.last().unwrap()] } else { Vec::new() };
32            let leading = if has_trailing_function { &current_order[..current_order.len() - 1] } else { &current_order[..] };
33            let (mut with_defaults, without_defaults): (Vec<KtParameter>, Vec<KtParameter>) =
34                leading.iter().partition(|it| it.has_default_value(ast));
35            // Stable, like `sortedWith`.
36            with_defaults.sort_by_key(|it| {
37                (Reverse(is_modifier(ast, it.node(), config)), Reverse(it.name(ast).as_deref() == Some("modifier")))
38            });
39            let proper_order: Vec<KtParameter> =
40                without_defaults.into_iter().chain(with_defaults).chain(trailing_lambda).collect();
41            let should_be_lenient = !has_trailing_function
42                && proper_order.last().is_some_and(|last_param| {
43                    let type_ref = last_param.type_reference(ast);
44                    last_param.has_default_value(ast)
45                        && type_ref.is_some_and(|t| {
46                            is_lambda(ast, t, &lambda_types)
47                                && !is_composable_lambda(ast, t, &lambda_types, &composable_lambda_types)
48                        })
49                });
50            if !should_be_lenient && current_order != proper_order {
51                let message = create_error_message(ast, &current_order, &proper_order);
52                emitter.report(ast, function.node(), &message, false);
53            }
54        }
55    }
56}
57
58fn join_texts(ast: &Ast, parameters: &[KtParameter]) -> String {
59    parameters.iter().map(|it| it.text(ast)).collect::<Vec<_>>().join(", ")
60}
61
62pub fn create_error_message(ast: &Ast, current_order: &[KtParameter], proper_order: &[KtParameter]) -> String {
63    create_error_message_text(&join_texts(ast, current_order), &join_texts(ast, proper_order))
64}
65
66/// The raw string is interpolated before `trimIndent()`, so multi-line parameter texts change the common indent.
67pub fn create_error_message_text(current_order: &str, proper_order: &str) -> String {
68    trim_indent(&format!(
69        "
70            Parameters in a composable function should be ordered following this pattern: params without defaults, modifiers, params with defaults and optionally, a trailing function that might not have a default param.
71            Current params are: [{current_order}] but could be [{proper_order}].
72            See https://mrmans0n.github.io/compose-rules/rules/#ordering-composable-parameters-properly for more information.
73        "
74    ))
75}
76
77/// Kotlin `String.trimIndent()`.
78fn trim_indent(s: &str) -> String {
79    let lines: Vec<&str> = s.split("\r\n").flat_map(|l| l.split(['\n', '\r'])).collect();
80    let is_blank = |l: &str| l.chars().all(char::is_whitespace);
81    let indent_width = |l: &str| l.chars().position(|c| !c.is_whitespace()).unwrap_or(l.chars().count());
82    let min_common_indent = lines.iter().filter(|l| !is_blank(l)).map(|l| indent_width(l)).min().unwrap_or(0);
83    let last_index = lines.len() - 1;
84    lines
85        .iter()
86        .enumerate()
87        .filter(|&(index, line)| !((index == 0 || index == last_index) && is_blank(line)))
88        .map(|(_, line)| line.chars().skip(min_common_indent).collect::<String>())
89        .collect::<Vec<_>>()
90        .join("\n")
91}