diff options
Diffstat (limited to 'mingling_cli')
| -rw-r--r-- | mingling_cli/src/linter/mlint_report.rs | 12 | ||||
| -rw-r--r-- | mingling_cli/src/lints/non_mingling_naming_style.rs | 4 | ||||
| -rw-r--r-- | mingling_cli/src/lints/unnecessary_render_result_creation.rs | 221 |
3 files changed, 228 insertions, 9 deletions
diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs index 8195f77..0472056 100644 --- a/mingling_cli/src/linter/mlint_report.rs +++ b/mingling_cli/src/linter/mlint_report.rs @@ -51,8 +51,8 @@ pub struct MlintReport { /// Compilation target source file path that this report belongs to pub target_src_path: Option<String>, - /// A suggestion for automatic fix (shown as diff in annotated output) - pub suggestion: Option<LintSuggestion>, + /// Suggestions for automatic fix (shown as diff in annotated output) + pub suggestions: Vec<LintSuggestion>, } /// Report severity level, indicating the seriousness of the Lint result. @@ -248,8 +248,8 @@ impl MlintReport { group = group.element(msg); } - // Render suggestion as a diff (Element::Suggestion) - if let Some(sugg) = &self.suggestion { + // Render suggestions as diffs + for sugg in &self.suggestions { let patch_snippet = Snippet::source(sugg.source.clone()) .line_start(sugg.line_start) .path(&self.file_name) @@ -404,8 +404,8 @@ impl MlintReport { .collect::<Vec<_>>(), ) .label(span.label.clone()) - .suggested_replacement(self.suggestion.as_ref().map(|s| s.replacement.clone())) - .suggestion_applicability(if self.suggestion.is_some() { + .suggested_replacement(self.suggestions.first().map(|s| s.replacement.clone())) + .suggestion_applicability(if !self.suggestions.is_empty() { Some(cargo_metadata::diagnostic::Applicability::MachineApplicable) } else { None diff --git a/mingling_cli/src/lints/non_mingling_naming_style.rs b/mingling_cli/src/lints/non_mingling_naming_style.rs index 6481926..83168b7 100644 --- a/mingling_cli/src/lints/non_mingling_naming_style.rs +++ b/mingling_cli/src/lints/non_mingling_naming_style.rs @@ -153,12 +153,12 @@ fn check_fn_name(func: &syn::ItemFn, source: &str) -> Vec<MlintReport> { lint_code: "non_mingling_naming_style".into(), message: msg, spans: vec![span], - suggestion: Some(LintSuggestion { + suggestions: vec![LintSuggestion { source: source_line.to_string(), line_start: func.sig.span().start().line, byte_range, replacement: suggestion_target, - }), + }], attached_reports: vec![MlintReport { level: MlintLevel::Help, message: format!("expected `{expected_fn}` ↔ `{expected_type}`"), diff --git a/mingling_cli/src/lints/unnecessary_render_result_creation.rs b/mingling_cli/src/lints/unnecessary_render_result_creation.rs index 03f81d2..1450c1a 100644 --- a/mingling_cli/src/lints/unnecessary_render_result_creation.rs +++ b/mingling_cli/src/lints/unnecessary_render_result_creation.rs @@ -14,8 +14,9 @@ //! Author: `Weicao-CatilGrass` //! Default: `warn` -use crate::linter::mlint_report::{MlintLevel, MlintReport}; +use crate::linter::mlint_report::{LintSuggestion, MlintLevel, MlintReport}; use quote::ToTokens; +use syn::spanned::Spanned; pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> { if !has_renderer_attr(&ast) { @@ -45,6 +46,31 @@ pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> { if only_print_and_return && print_count > 0 { let span = MlintReport::span_from_syn(&ast.sig, source); + let mut suggestions = Vec::new(); + + // 1. Attribute change: #[renderer] → #[renderer(buffer)] + if let Some(sugg) = make_attr_suggestion(&ast, source) { + suggestions.push(sugg); + } + + // 2. Remove -> RenderResult from function signature + if let Some(sugg) = make_return_type_suggestion(&ast, source) { + suggestions.push(sugg); + } + + // 3. Remove let mut r = RenderResult::... + if let Some(sugg) = make_let_removal_suggestion(stmts, let_idx, source) { + suggestions.push(sugg); + } + + // 4. Fix r_println!(r, ...) → r_println!(...) for all r_xxx macros + suggestions.extend(make_macro_arg_suggestions(stmts, &r_name, source)); + + // 5. Remove return 'r' expression + if let Some(sugg) = make_return_removal_suggestion(stmts, &r_name, source) { + suggestions.push(sugg); + } + vec![MlintReport { source_code: source.to_string(), level: MlintLevel::Warning, @@ -54,6 +80,7 @@ pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> { ast.sig.ident, ), spans: vec![span], + suggestions, attached_reports: vec![MlintReport { level: MlintLevel::Help, message: format!( @@ -185,6 +212,198 @@ fn check_stmt_usage(stmt: &syn::Stmt, r_name: &str, print_count: &mut usize) -> true } +/// Build suggestion: `#[renderer]` → `#[renderer(buffer)]` +fn make_attr_suggestion(ast: &syn::ItemFn, source: &str) -> Option<LintSuggestion> { + let attr = ast.attrs.iter().find(|a| { + let name = a.path().to_token_stream().to_string(); + name.ends_with("renderer") + })?; + + let line_idx = attr.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + // Replace `renderer]` with `renderer(buffer)]` + // This handles both `#[renderer]` and `#[::mingling::macros::renderer]` + let line_str = line; + let replacement = line_str.replacen("renderer]", "renderer(buffer)]", 1); + + if replacement == line_str { + return None; + } + + Some(LintSuggestion { + source: line_str.to_string(), + line_start: line_idx + 1, + byte_range: 0..line_str.len(), + replacement, + }) +} + +/// Build suggestion: remove ` -> RenderResult` from function signature +fn make_return_type_suggestion(ast: &syn::ItemFn, source: &str) -> Option<LintSuggestion> { + let syn::ReturnType::Type(arrow, ret_type) = &ast.sig.output else { + return None; + }; + + let sig_line_idx = ast.sig.span().start().line.saturating_sub(1); + let line = source.lines().nth(sig_line_idx)?; + + // proc-macro2 column is 0-based byte offset from line start + let arrow_byte_col = arrow.span().start().column; + let ret_end_byte_col = ret_type.span().end().column; + + // Include the space before `->` + let range_start = if arrow_byte_col > 0 { + arrow_byte_col - 1 + } else { + arrow_byte_col + }; + + Some(LintSuggestion { + source: line.to_string(), + line_start: sig_line_idx + 1, + byte_range: range_start..ret_end_byte_col, + replacement: String::new(), + }) +} + +/// Build suggestion: remove `let mut r = RenderResult::...` line +fn make_let_removal_suggestion( + stmts: &[syn::Stmt], + let_idx: usize, + source: &str, +) -> Option<LintSuggestion> { + let stmt = &stmts[let_idx]; + let line_idx = stmt.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + Some(LintSuggestion { + source: line.to_string(), + line_start: line_idx + 1, + byte_range: 0..line.len(), + replacement: String::new(), + }) +} + +/// Build suggestions: fix `r_println!(r, ...)` → `r_println!(...)` for all r_xxx macros +fn make_macro_arg_suggestions( + stmts: &[syn::Stmt], + r_name: &str, + source: &str, +) -> Vec<LintSuggestion> { + let r_macros = ["r_println", "r_eprintln", "r_print", "r_eprint"]; + + stmts + .iter() + .filter_map(|stmt| { + let syn::Stmt::Macro(stmt_mac) = stmt else { + return None; + }; + + let macro_name = stmt_mac + .mac + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default(); + + if !r_macros.contains(¯o_name.as_str()) { + return None; + } + + // Check that the first token is the r_name + let first_token = stmt_mac.mac.tokens.clone().into_iter().next()?; + if first_token.to_string() != *r_name { + return None; + } + + let line_idx = stmt.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + // Find pattern: macro_name!(r_name, ... + let macro_str = format!("{}!(", macro_name); + let macro_pos = line.find(¯o_str)?; + let after_open = macro_pos + macro_str.len(); + + // The first argument is `r_name` followed by `,` and possibly a space + // We need to find and remove `r_name, ` or `r_name,` + let first_arg = r_name; + if line[after_open..].starts_with(first_arg) { + // Find the end of the first argument (including `,` and any whitespace) + let arg_end = after_open + first_arg.len(); + if arg_end < line.len() { + let rest = &line[arg_end..]; + // Skip `,` and optional whitespace + let skip = if rest.starts_with(", ") { + 2 + } else if rest.starts_with(',') { + 1 + } else { + // Not followed by comma — not our pattern + return None; + }; + let range_end = arg_end + skip; + + Some(LintSuggestion { + source: line.to_string(), + line_start: line_idx + 1, + byte_range: after_open..range_end, + replacement: String::new(), + }) + } else { + None + } + } else { + None + } + }) + .collect() +} + +/// Build suggestion: remove the return `r` expression +fn make_return_removal_suggestion( + stmts: &[syn::Stmt], + r_name: &str, + source: &str, +) -> Option<LintSuggestion> { + let last = stmts.last()?; + + let is_r_return = match last { + // `r` (bare expression, no semicolon) or `r;` (with semicolon) + syn::Stmt::Expr(expr, _) => { + if let syn::Expr::Path(p) = expr { + p.path.is_ident(r_name) + } else if let syn::Expr::Return(ret) = expr { + ret.expr.as_ref().is_some_and(|e| { + if let syn::Expr::Path(p) = e.as_ref() { + p.path.is_ident(r_name) + } else { + false + } + }) + } else { + false + } + } + _ => false, + }; + + if !is_r_return { + return None; + } + + let line_idx = last.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + Some(LintSuggestion { + source: line.to_string(), + line_start: line_idx + 1, + byte_range: 0..line.len(), + replacement: String::new(), + }) +} + #[cfg(test)] mod lint_test { use crate::{assert_detected, assert_not_detected}; |
