ktrs_compose/rules/
material2.rs1use 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
71fn 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
80fn 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.";