aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_cli/src')
-rw-r--r--mingling_cli/src/bin/wrapper.rs47
-rw-r--r--mingling_cli/src/errors.rs2
-rw-r--r--mingling_cli/src/errors/io_error.rs8
-rw-r--r--mingling_cli/src/linter.rs13
-rw-r--r--mingling_cli/src/linter/cmd_mlint.rs73
-rw-r--r--mingling_cli/src/linter/mlint_report.rs12
-rw-r--r--mingling_cli/src/lints.rs2
-rw-r--r--mingling_cli/src/lints/non_mingling_naming_style.rs4
-rw-r--r--mingling_cli/src/lints/template_linter.rs2
-rw-r--r--mingling_cli/src/lints/unnecessary_render_result_creation.rs221
10 files changed, 336 insertions, 48 deletions
diff --git a/mingling_cli/src/bin/wrapper.rs b/mingling_cli/src/bin/wrapper.rs
new file mode 100644
index 0000000..b1aecf6
--- /dev/null
+++ b/mingling_cli/src/bin/wrapper.rs
@@ -0,0 +1,47 @@
+use std::env;
+use std::ffi::OsString;
+use std::path::PathBuf;
+use std::process::{self, Command};
+
+fn main() {
+ exec();
+}
+
+fn exec() {
+ fn target_exe_path() -> PathBuf {
+ let mut path = env::current_exe().expect("Fail to read current_exe");
+ path.pop();
+
+ let target_name = if cfg!(target_os = "windows") {
+ "mingling-cli.exe"
+ } else {
+ "mingling-cli"
+ };
+
+ path.push(target_name);
+ path
+ }
+
+ let mut args: Vec<OsString> = env::args_os().collect();
+ let pass_args = if args.len() > 1 {
+ args.split_off(1)
+ } else {
+ vec![]
+ };
+
+ let target = target_exe_path();
+
+ let status = Command::new(&target)
+ .args(&pass_args)
+ .status()
+ .unwrap_or_else(|_| {
+ process::exit(1);
+ });
+
+ match status.code() {
+ Some(code) => process::exit(code),
+ None => {
+ process::exit(1);
+ }
+ }
+}
diff --git a/mingling_cli/src/errors.rs b/mingling_cli/src/errors.rs
index cef9616..5cdc43f 100644
--- a/mingling_cli/src/errors.rs
+++ b/mingling_cli/src/errors.rs
@@ -1 +1,3 @@
+pub mod io_error;
pub mod serde_json;
+
diff --git a/mingling_cli/src/errors/io_error.rs b/mingling_cli/src/errors/io_error.rs
new file mode 100644
index 0000000..97509c9
--- /dev/null
+++ b/mingling_cli/src/errors/io_error.rs
@@ -0,0 +1,8 @@
+use mingling::macros::{group, renderer};
+
+group!(ErrorIo = std::io::Error);
+
+#[renderer]
+pub fn handle_error_io(err: ErrorIo) {
+ panic!("{}", err.to_string())
+}
diff --git a/mingling_cli/src/linter.rs b/mingling_cli/src/linter.rs
index c85afc1..859ffc5 100644
--- a/mingling_cli/src/linter.rs
+++ b/mingling_cli/src/linter.rs
@@ -4,7 +4,7 @@ use mingling::{
};
use crate::{
- linter::cmd_mlint::{CMDMinglingLinter, EntryMinglingLinter},
+ linter::cmd_mlint::{CMDLint, EntryLint},
metadata::setup::ResUsingJson,
};
@@ -19,7 +19,7 @@ pub fn mingling_linter_setup(program: &mut Program<crate::ThisProgram>) {
#[program_setup]
pub fn mingling_linter_command_setup(program: &mut Program<crate::ThisProgram>) {
- program.with_dispatcher(CMDMinglingLinter);
+ program.with_dispatcher(CMDLint);
program.with_dispatcher(CMDLinterSupportRustAnalyzer);
program.with_dispatcher(CMDLinterSupportRustAnalyzerWithClippy);
program.with_dispatcher(CMDLinterSupportRustAnalyzerWithCheck);
@@ -40,10 +40,7 @@ dispatcher!("ra-lint",
);
#[chain]
-pub fn handle_ra_lint(
- _: EntryLinterSupportRustAnalyzer,
- use_json: &mut ResUsingJson,
-) -> EntryMinglingLinter {
+pub fn handle_ra_lint(_: EntryLinterSupportRustAnalyzer, use_json: &mut ResUsingJson) -> EntryLint {
use_json.using = true;
entry!("--message-format=json")
}
@@ -52,7 +49,7 @@ pub fn handle_ra_lint(
pub fn handle_ra_lint_check(
_: EntryLinterSupportRustAnalyzerWithCheck,
use_json: &mut ResUsingJson,
-) -> EntryMinglingLinter {
+) -> EntryLint {
use_json.using = true;
entry!("--message-format=json", "--with-checker=cargo,check")
}
@@ -61,7 +58,7 @@ pub fn handle_ra_lint_check(
pub fn handle_ra_lint_clippy(
_: EntryLinterSupportRustAnalyzerWithClippy,
use_json: &mut ResUsingJson,
-) -> EntryMinglingLinter {
+) -> EntryLint {
use_json.using = true;
entry!("--message-format=json", "--with-checker=cargo,clippy")
}
diff --git a/mingling_cli/src/linter/cmd_mlint.rs b/mingling_cli/src/linter/cmd_mlint.rs
index 90871af..1dfd02b 100644
--- a/mingling_cli/src/linter/cmd_mlint.rs
+++ b/mingling_cli/src/linter/cmd_mlint.rs
@@ -4,8 +4,9 @@ use mingling::LazyRes;
use mingling::consts::REMAINS;
use mingling::macros::{arg, chain, dispatcher, pack};
use mingling::picker::EntryPicker;
+use tokio::task::JoinSet;
-dispatcher!("lint", CMDMinglingLinter => EntryMinglingLinter);
+dispatcher!("lint", CMDLint => EntryLint);
/// Main linting function that processes all packages in the metadata.
///
@@ -13,42 +14,54 @@ dispatcher!("lint", CMDMinglingLinter => EntryMinglingLinter);
/// reads Rust source files (`.rs`), parses them into ASTs, runs lint checks,
/// and enriches each report with metadata information.
async fn linter_main(metadata: &Metadata) -> Vec<MlintReport> {
- // Vector to accumulate all lint reports
- let mut all_reports = Vec::new();
+ let mut join_set = JoinSet::new();
- // Iterate over all packages in the metadata
for package in &metadata.packages {
- // Iterate over all targets within a package (e.g., bin, lib, test, etc.)
for target in &package.targets {
let path = &target.src_path;
// Only process Rust source files (with `.rs` extension)
if !path.as_str().ends_with(".rs") {
continue;
}
- // Read the source file content
- let source = match std::fs::read_to_string(path.as_str()) {
- Ok(s) => s,
- Err(_) => continue,
- };
- // Parse the source file into an AST (Abstract Syntax Tree)
- let ast = match syn::parse_file(&source) {
- Ok(f) => f,
- Err(_) => continue,
- };
-
- // Run all lint checks and collect reports
- let reports = crate::lints::run_all_lints(&ast, &source);
- //
- for mut r in reports {
- // Enrich report with metadata information
- r.file_name = path.as_str().to_string();
- r.source_code = source.clone();
- r.package_id = Some(package.id.to_string());
- r.target_name = Some(target.name.clone());
- r.target_kind = target.kind.first().map(|k| k.to_string());
- r.target_src_path = Some(path.as_str().to_string());
- all_reports.push(r);
- }
+
+ // Clone/move all data needed inside the blocking closure
+ let path_str = path.as_str().to_string();
+ let package_id = package.id.to_string();
+ let target_name = target.name.clone();
+ let target_kind = target.kind.first().map(|k| k.to_string());
+
+ join_set.spawn_blocking(move || {
+ // Read the source file content
+ let source = std::fs::read_to_string(&path_str).ok()?;
+ // Parse the source file into an AST
+ let ast = syn::parse_file(&source).ok()?;
+ // Run all lint checks and collect reports
+ let reports = crate::lints::run_all_lints(&ast, &source);
+
+ // Enrich each report with metadata information
+ let enriched: Vec<MlintReport> = reports
+ .into_iter()
+ .map(|mut r| {
+ r.file_name = path_str.clone();
+ r.source_code = source.clone();
+ r.package_id = Some(package_id.clone());
+ r.target_name = Some(target_name.clone());
+ r.target_kind = target_kind.clone();
+ r.target_src_path = Some(path_str.clone());
+ r
+ })
+ .collect();
+
+ Some(enriched)
+ });
+ }
+ }
+
+ let mut all_reports = Vec::new();
+ while let Some(res) = join_set.join_next().await {
+ // `spawn_blocking` panics are propagated, `None` means task skipped (read/parse failure)
+ if let Ok(Some(reports)) = res {
+ all_reports.extend(reports);
}
}
@@ -58,7 +71,7 @@ async fn linter_main(metadata: &Metadata) -> Vec<MlintReport> {
pack!(StateBeginLinter = ());
#[chain]
-pub fn handle_lint(args: EntryMinglingLinter) -> StateBeginLinter {
+pub fn handle_lint(args: EntryLint) -> StateBeginLinter {
let (with_checker, checker_args) = args
.pick_or(&arg![with_checker: Option<String>], || {
Some("cargo,check".to_string())
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.rs b/mingling_cli/src/lints.rs
index 89f5ca3..8cc1e6e 100644
--- a/mingling_cli/src/lints.rs
+++ b/mingling_cli/src/lints.rs
@@ -1,3 +1,5 @@
+// This file is auto-generated by pre/lint_registry.rs
+
#![allow(unused)]
use crate::linter::mlint_report::{MlintLevel, MlintReport};
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/template_linter.rs b/mingling_cli/src/lints/template_linter.rs
index 271aa0c..4836e24 100644
--- a/mingling_cli/src/lints/template_linter.rs
+++ b/mingling_cli/src/lints/template_linter.rs
@@ -13,7 +13,7 @@
//! > 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`
+//! Author: `Your-Name`
//! Default: `allow`
// ^^^^^ Supported parameters: `warn`, `allow`, `deny`
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};