diff options
Diffstat (limited to 'mingling_cli/src/lints/unnecessary_render_result_creation.rs')
| -rw-r--r-- | mingling_cli/src/lints/unnecessary_render_result_creation.rs | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/mingling_cli/src/lints/unnecessary_render_result_creation.rs b/mingling_cli/src/lints/unnecessary_render_result_creation.rs new file mode 100644 index 0000000..fa6809b --- /dev/null +++ b/mingling_cli/src/lints/unnecessary_render_result_creation.rs @@ -0,0 +1,157 @@ +//! Unnecessary Manual RenderResult Creation +//! +//! ## Summary +//! +//! Detects `#[renderer]` functions that manually create a `RenderResult` and +//! manage it via `r_println!(r, ...)` style calls, when they could be simplified +//! to `#[renderer(buffer)]` which handles the buffer automatically. +//! +//! This lint will not trigger if `r` is used outside of `r_println!`, +//! `r_eprintln!`, `r_print`, `r_eprint`, or `r_append`. +//! +//! ## Metadata +//! +//! Author: `Weicao-CatilGrass` +//! Default: `warn` + +use crate::linter::mlint_report::{MlintLevel, MlintReport}; + +pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> { + if !has_renderer_attr(&ast) { + return vec![]; + } + + let stmts = &ast.block.stmts; + if stmts.len() < 2 { + return vec![]; + } + + let (r_ident, let_idx) = match find_render_result_new(stmts) { + Some(pair) => pair, + None => return vec![], + }; + + let r_name = r_ident.to_string(); + let mut only_print_and_return = true; + let mut print_count = 0; + + for stmt in &stmts[let_idx + 1..] { + if !check_stmt_usage(stmt, &r_name, &mut print_count) { + only_print_and_return = false; + break; + } + } + + if only_print_and_return && print_count > 0 { + let span = MlintReport::span_from_syn(&ast.sig, source); + vec![MlintReport { + file_name: String::new(), + source_code: source.to_string(), + level: MlintLevel::Warning, + lint_code: "unnecessary_render_result_creation".into(), + message: format!( + "unnecessary manual `RenderResult` creation in `{}`: use `#[renderer(buffer)]` instead", + ast.sig.ident, + ), + spans: vec![span], + attached_reports: vec![MlintReport { + file_name: String::new(), + source_code: String::new(), + level: MlintLevel::Help, + lint_code: String::new(), + message: "change to `#[renderer(buffer)]` and use `r_println!(...)` without the `r` parameter".into(), + spans: vec![], + attached_reports: vec![], + package_id: None, + target_name: None, + target_kind: None, + target_src_path: None, + }], + package_id: None, + target_name: None, + target_kind: None, + target_src_path: None, + }] + } else { + vec![] + } +} + +fn has_renderer_attr(func: &syn::ItemFn) -> bool { + func.attrs.iter().any(|a| { + if !a.path().is_ident("renderer") { + return false; + } + if let Ok(list) = a.meta.require_list() { + let tokens = list.tokens.to_string(); + if tokens.contains("buffer") { + return false; + } + } + true + }) +} + +fn find_render_result_new(stmts: &[syn::Stmt]) -> Option<(proc_macro2::Ident, usize)> { + for (i, stmt) in stmts.iter().enumerate() { + if let syn::Stmt::Local(local) = stmt + && let Some(init) = &local.init + && let syn::Pat::Ident(pat_id) = &local.pat + && pat_id.mutability.is_some() + && let syn::Expr::Call(call) = &*init.expr + && let syn::Expr::Path(expr_path) = call.func.as_ref() + { + let segs = &expr_path.path.segments; + let matches = match segs.len() { + 2 => segs[0].ident == "RenderResult" && segs[1].ident == "new", + 3 => { + segs[0].ident == "mingling" + && segs[1].ident == "RenderResult" + && segs[2].ident == "new" + } + _ => false, + }; + if matches { + return Some((pat_id.ident.clone(), i)); + } + } + } + None +} + +fn check_stmt_usage(stmt: &syn::Stmt, r_name: &str, print_count: &mut usize) -> bool { + // return r; → allowed + if let syn::Stmt::Expr(expr, _) = stmt + && let syn::Expr::Return(ret) = expr + && let Some(ret_expr) = &ret.expr + && let syn::Expr::Path(p) = ret_expr.as_ref() + && p.path.is_ident(r_name) + { + return true; + } + + // r_println!(r, ...) → allowed + if let syn::Stmt::Macro(stmt_mac) = stmt { + let macro_name = stmt_mac + .mac + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default(); + let is_r_macro = matches!( + macro_name.as_str(), + "r_println" | "r_eprintln" | "r_print" | "r_eprint" | "r_append" + ); + if is_r_macro + && let Some(first_arg) = stmt_mac.mac.tokens.clone().into_iter().next() + && first_arg.to_string() == *r_name + { + *print_count += 1; + return true; + } + } + + let s = format!("{stmt:#?}"); + !s.contains(&format!("\"{r_name}\"")) +} |
