aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mingling_cli/src/linter/mlint_report.rs12
-rw-r--r--mingling_cli/src/lints/non_mingling_naming_style.rs4
-rw-r--r--mingling_cli/src/lints/unnecessary_render_result_creation.rs221
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(&macro_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(&macro_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};