Skip to main content

ktrs_compose/rules/
material2.rs

1//! Port of `rules/Material2.kt`.
2
3use ktrs_ast::Ast;
4use ktrs_ast::psi::{FqName, KtCallExpression, KtDotQualifiedExpression, KtFile, KtReferenceExpression};
5
6use crate::core::compose_kt_config::ComposeKtConfig;
7use crate::core::compose_kt_visitor::ComposeKtVisitor;
8use crate::core::emitter::Emitter;
9use crate::core::util::kotlin_utils::fq_name_plus;
10use crate::core::util::kt_dot_qualified_expressions::dedup_using_outermost;
11use crate::core::util::psi_elements::{find_all_children, range};
12
13pub struct Material2;
14
15impl ComposeKtVisitor for Material2 {
16    fn is_opt_in(&self) -> bool {
17        true
18    }
19
20    fn visit_file(&self, ast: &mut Ast, file: KtFile, emitter: &mut dyn Emitter, config: &dyn ComposeKtConfig) {
21        let m2 = m2_fq_name();
22        let mut allowed_fq_names: Vec<FqName> =
23            config.get_set("allowedFromM2", &[]).iter().map(|it| fq_name_plus(&m2, it)).collect();
24        allowed_fq_names.push(fq_name_plus(&m2, "icons"));
25        let import_list = file.import_list(ast);
26        let imports: Vec<_> = import_list
27            .map(|it| it.imports(ast))
28            .unwrap_or_default()
29            .into_iter()
30            .filter(|it| it.imported_fq_name(ast).is_some_and(|fq| starts_with(&fq, &m2)))
31            .filter(|directive| {
32                !allowed_fq_names.iter().any(|it| directive.imported_fq_name(ast).is_some_and(|fq| starts_with(&fq, it)))
33            })
34            .collect();
35        for directive in imports {
36            emitter.report(ast, directive.node(), DISALLOWED_USAGE_OF_MATERIAL2, false);
37        }
38        let mut dot_qualified = find_all_children::<KtDotQualifiedExpression>(ast, file.node());
39        if let Some(imps) = import_list {
40            let imps = range(ast, imps.node());
41            dot_qualified.retain(|it| !imps.contains(&ast.start_offset(it.node())));
42        }
43        if let Some(pkg) = file.package_directive(ast) {
44            let pkg = range(ast, pkg.node());
45            dot_qualified.retain(|it| !pkg.contains(&ast.start_offset(it.node())));
46        }
47        let references: Vec<_> = dedup_using_outermost(ast, &dot_qualified)
48            .into_iter()
49            .filter(|it| has_reference_to_m2(ast, *it, &m2, &allowed_fq_names))
50            .collect();
51        for reference in references {
52            emitter.report(ast, reference.node(), DISALLOWED_USAGE_OF_MATERIAL2, false);
53        }
54    }
55}
56
57fn has_reference_to_m2(ast: &Ast, expression: KtDotQualifiedExpression, m2: &FqName, allowlist: &[FqName]) -> bool {
58    let Some(selector) = expression.selector_expression(ast) else { return false };
59    let fqn = if let Some(call) = KtCallExpression::cast(ast, selector) {
60        let receiver = expression.receiver_expression(ast).expect("NullPointerException: receiverExpression");
61        let callee = call.callee_expression(ast).map_or_else(|| "null".to_owned(), |c| ast.text(c));
62        FqName::new(&format!("{}.{callee}", ast.text(receiver)))
63    } else if KtReferenceExpression::is(ast, selector) {
64        FqName::new(&expression.text(ast))
65    } else {
66        return false;
67    };
68    starts_with(&fqn, m2) && !allowlist.iter().any(|it| starts_with(&fqn, it))
69}
70
71/// `FqName.startsWith(FqName)` (`FqNameUnsafe.startsWith`): equal, or `other` followed by a `.`; never for root.
72fn starts_with(fq_name: &FqName, other: &FqName) -> bool {
73    let (this, other) = (fq_name.as_string(), other.as_string());
74    !this.is_empty()
75        && this.len() >= other.len()
76        && (this.len() == other.len() || this.as_bytes()[other.len()] == b'.')
77        && this.as_bytes().starts_with(other.as_bytes())
78}
79
80/// `FqName.fromSegments(listOf("androidx", "compose", "material"))`.
81fn m2_fq_name() -> FqName {
82    FqName::new("androidx.compose.material")
83}
84
85pub const DISALLOWED_USAGE_OF_MATERIAL2: &str = "\
86Compose Material 2 is disallowed by your configuration.
87See https://mrmans0n.github.io/compose-rules/rules/#dont-use-material-2 for more information.";