aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/lints
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-21 15:10:38 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-21 15:12:52 +0800
commit9976b31a56bcc3ed37ac878b351bd8d097dc67cc (patch)
treee7b092aee5e82f5d4c1f50332a17f1d2176ae51b /mingling_cli/src/lints
parentee01756279ef17487d19659adedc0639547bbd25 (diff)
feat(mling): add linter framework with async support and registry
generation
Diffstat (limited to 'mingling_cli/src/lints')
-rw-r--r--mingling_cli/src/lints/template_linter.rs48
-rw-r--r--mingling_cli/src/lints/unnecessary_render_result_creation.rs157
2 files changed, 205 insertions, 0 deletions
diff --git a/mingling_cli/src/lints/template_linter.rs b/mingling_cli/src/lints/template_linter.rs
new file mode 100644
index 0000000..4ea2f94
--- /dev/null
+++ b/mingling_cli/src/lints/template_linter.rs
@@ -0,0 +1,48 @@
+//! Template Linter
+//!
+//! ## Summary
+//!
+//! This is a template Linter that introduces how to add a Lint for Mingling.
+//! You can write an introduction for this Linter in the Summary section, for example:
+//!
+//! - Trigger conditions
+//! - Why is it necessary?
+//!
+//! ## Metadata
+//!
+//! > This section is the **Metadata** section, which needs to be filled in correctly.
+//! > These contents will eventually be compiled as the Linter's behavior.
+//!
+//! Author: `Weicao-CatilGrass`
+//! Default: `allow`
+// ^^^^^ Supported parameters: `warn`, `allow`, `deny`
+
+// --- ABOUT AUTO IDENTIFICATION RULES ---
+//
+// The compiler will treat code with the following structure as a Linter entry point:
+// |
+// --> your_linter_module.rs
+// |
+// | pub fn linter(ast: syn::ItemX) -> Vec<MlintReport> {
+// | /* ... */ ^^^^^^^^^^ Your linter scope
+// | }
+// |
+// = note: Please ensure your function is `pub`, named `linter`, and returns `Vec<MlintReport>`
+//
+// --- ABOUT AUTO IDENTIFICATION RULES ---
+
+use crate::linter::mlint_report::MlintReport;
+
+pub fn linter(_ast: syn::ItemFn, _source: &str) -> Vec<MlintReport> {
+ // ^^^^^^^^^^^ Supported parameters:
+ // | syn::File
+ // | syn::ItemImpl
+ // | syn::ItemStruct
+ // | syn::ItemEnum
+ // | syn::ItemTrait
+ // | syn::ItemFn
+ // | syn::ItemMacro
+ // | syn::ItemMod
+ // | syn::ItemUnion
+ vec![]
+}
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}\""))
+}