aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/linter/mlint_report.rs
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/linter/mlint_report.rs
parentee01756279ef17487d19659adedc0639547bbd25 (diff)
feat(mling): add linter framework with async support and registry
generation
Diffstat (limited to 'mingling_cli/src/linter/mlint_report.rs')
-rw-r--r--mingling_cli/src/linter/mlint_report.rs413
1 files changed, 413 insertions, 0 deletions
diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs
new file mode 100644
index 0000000..3c52828
--- /dev/null
+++ b/mingling_cli/src/linter/mlint_report.rs
@@ -0,0 +1,413 @@
+use cargo_metadata::diagnostic::{
+ DiagnosticCodeBuilder, DiagnosticLevel as CargoLevel, DiagnosticSpanBuilder,
+ DiagnosticSpanLineBuilder,
+};
+
+use cargo_metadata::{Message, PackageId};
+
+use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING};
+use annotate_snippets::{AnnotationKind, Group, Renderer, Snippet};
+use mingling::macros::{buffer, chain, pack, r_append, r_eprintln, renderer};
+use mingling::{AnyOutput, ProgramCollect, Routable};
+
+use crate::Next;
+use crate::metadata::setup::ResUsingJson;
+
+/// Complete structure of a Lint report, containing inspection results and associated metadata.
+pub struct MlintReport {
+ /// Source file name
+ pub file_name: String,
+
+ /// Full source text of the file, used to extract line content and compute byte offsets
+ pub source_code: String,
+
+ /// Severity level of the report
+ pub level: MlintLevel,
+
+ /// Name of the Lint
+ pub lint_code: String,
+
+ /// Content of the report
+ pub message: String,
+
+ /// Source code locations
+ pub spans: Vec<LintSpan>,
+
+ /// Attached sub-reports for this report
+ pub attached_reports: Vec<MlintReport>,
+
+ /// Package ID that this report belongs to
+ pub package_id: Option<String>,
+
+ /// Compilation target name that this report belongs to
+ pub target_name: Option<String>,
+
+ /// Compilation target type that this report belongs to
+ pub target_kind: Option<String>,
+
+ /// Compilation target source file path that this report belongs to
+ pub target_src_path: Option<String>,
+}
+
+/// Report severity level, indicating the seriousness of the Lint result.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum MlintLevel {
+ Error,
+ Warning,
+ Note,
+ Help,
+}
+
+/// Source code location span, representing a range of source code and its associated text information.
+pub struct LintSpan {
+ /// Starting line number (1-based)
+ pub line_start: usize,
+ /// Ending line number (1-based)
+ pub line_end: usize,
+ /// Starting column (1-based char offset)
+ pub column_start: usize,
+ /// Ending column (1-based char offset)
+ pub column_end: usize,
+ /// Source lines at this location
+ pub text: Vec<LintSpanLine>,
+ /// Optional label description
+ pub label: Option<String>,
+}
+
+/// A single line of text in a source location with highlight range.
+pub struct LintSpanLine {
+ /// Full source line text (no trailing `\n`)
+ pub text: String,
+ /// Highlight start (1-based char offset)
+ pub highlight_start: usize,
+ /// Highlight end (1-based char offset)
+ pub highlight_end: usize,
+}
+
+impl MlintReport {
+ /// Build a `LintSpan` from a syn spanned item and the source text.
+ pub fn span_from_syn<T: syn::spanned::Spanned>(value: &T, source: &str) -> LintSpan {
+ let span = value.span();
+ let start = span.start();
+ let end = span.end();
+
+ // Extract line content from source
+ let lines: Vec<&str> = source.lines().collect();
+ let text = if start.line == end.line && start.line <= lines.len() {
+ let line_text = lines[start.line.saturating_sub(1)];
+ let hl_start = proc_macro2_byte_col_to_char_1based(line_text, start.column);
+ let hl_end = proc_macro2_byte_col_to_char_1based(line_text, end.column);
+ vec![LintSpanLine {
+ text: line_text.to_string(),
+ highlight_start: hl_start,
+ highlight_end: hl_end,
+ }]
+ } else {
+ // Multi-line: generate line by line
+ (start.line..=end.line.min(lines.len()))
+ .map(|i| {
+ let line_text = lines[i.saturating_sub(1)];
+ let (hl_start, hl_end) = if i == start.line {
+ (
+ proc_macro2_byte_col_to_char_1based(line_text, start.column),
+ line_text.chars().count(),
+ )
+ } else if i == end.line {
+ (
+ 1,
+ proc_macro2_byte_col_to_char_1based(line_text, end.column),
+ )
+ } else {
+ (1, line_text.chars().count())
+ };
+ LintSpanLine {
+ text: line_text.to_string(),
+ highlight_start: hl_start,
+ highlight_end: hl_end,
+ }
+ })
+ .collect::<Vec<_>>()
+ };
+
+ LintSpan {
+ line_start: start.line,
+ line_end: end.line,
+ column_start: proc_macro2_byte_col_to_char_1based(
+ lines.get(start.line.saturating_sub(1)).unwrap_or(&""),
+ start.column,
+ ),
+ column_end: proc_macro2_byte_col_to_char_1based(
+ lines.get(end.line.saturating_sub(1)).unwrap_or(&""),
+ end.column,
+ ),
+ text,
+ label: None,
+ }
+ }
+
+ /// Compute byte offset from (line, column) within source.
+ /// line: 1-based, column: 1-based char offset.
+ pub fn line_col_to_byte_offset(&self, line: usize, col: usize) -> usize {
+ let mut byte_pos = 0usize;
+ for (i, line_str) in self.source_code.lines().enumerate() {
+ if i + 1 == line {
+ return byte_pos + char_1based_to_byte_offset(line_str, col);
+ }
+ byte_pos += line_str.len() + 1; // +1 for \n
+ }
+ self.source_code.len()
+ }
+}
+
+/// proc-macro2's LineColumn.column is **0-based byte offset**.
+/// Convert to 1-based char offset.
+fn proc_macro2_byte_col_to_char_1based(line: &str, byte_col: usize) -> usize {
+ line.char_indices()
+ .position(|(i, _)| i >= byte_col)
+ .map(|pos| pos + 1) // → 1-based
+ .unwrap_or(line.chars().count().max(1))
+}
+
+/// 1-based char offset → byte offset within a string
+fn char_1based_to_byte_offset(s: &str, char_1based: usize) -> usize {
+ s.char_indices()
+ .nth(char_1based.saturating_sub(1))
+ .map(|(i, _)| i)
+ .unwrap_or(s.len())
+}
+
+impl MlintReport {
+ pub fn to_annotate_snippet_render(&self) -> String {
+ let level = mlinit_level_to_annotate(&self.level);
+ let title = level.clone().primary_title(&self.message);
+ // code 不放在 title 里,改放在 note 中
+ let mut group = Group::with_title(title);
+
+ for span in &self.spans {
+ let source = span
+ .text
+ .iter()
+ .map(|l| l.text.as_str())
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let byte_range = if span.text.len() == 1 {
+ let line = &span.text[0];
+ let start = char_1based_to_byte_offset(&line.text, line.highlight_start);
+ let end = char_1based_to_byte_offset(&line.text, line.highlight_end);
+ start..end
+ } else {
+ let first = &span.text[0];
+ let last = span.text.last().unwrap();
+ let start = char_1based_to_byte_offset(&first.text, first.highlight_start);
+ let prefix_len: usize = span.text[..span.text.len() - 1]
+ .iter()
+ .map(|l| l.text.len() + 1)
+ .sum();
+ let end = prefix_len + char_1based_to_byte_offset(&last.text, last.highlight_end);
+ start..end
+ };
+
+ let mut snippet = Snippet::source(source)
+ .line_start(span.line_start)
+ .path(&self.file_name);
+
+ let annotation = match &span.label {
+ Some(label) => AnnotationKind::Primary
+ .span(byte_range)
+ .label(label.as_str()),
+ None => AnnotationKind::Primary.span(byte_range),
+ };
+ snippet = snippet.annotation(annotation);
+ group = group.element(snippet);
+ }
+
+ for child in &self.attached_reports {
+ let child_level = mlinit_level_to_annotate(&child.level);
+ let msg = child_level.clone().message(&child.message);
+ group = group.element(msg);
+ }
+
+ // 添加 note: `#[mlint(level([code]))]` on by default
+ if !self.lint_code.is_empty() {
+ let level_name = match self.level {
+ MlintLevel::Error => "deny",
+ MlintLevel::Warning => "warn",
+ MlintLevel::Note | MlintLevel::Help => "allow",
+ };
+ let note_text = format!(
+ "`#[mlint({level_name}([{}]))]` on by default",
+ self.lint_code
+ );
+ let note_msg = NOTE.clone().message(note_text);
+ group = group.element(note_msg);
+ }
+
+ let renderer = Renderer::styled();
+ renderer.render(&[group])
+ }
+}
+
+fn mlinit_level_to_annotate(level: &MlintLevel) -> &'static annotate_snippets::Level<'static> {
+ match level {
+ MlintLevel::Error => &ERROR,
+ MlintLevel::Warning => &WARNING,
+ MlintLevel::Note => &NOTE,
+ MlintLevel::Help => &HELP,
+ }
+}
+
+impl MlintReport {
+ pub fn to_compiler_message(&self) -> Message {
+ use cargo_metadata::{CompilerMessageBuilder, Edition, TargetBuilder};
+
+ let target_kind_str = self.target_kind.as_deref().unwrap_or("bin");
+ let target_kind_parsed: cargo_metadata::TargetKind = target_kind_str.into();
+ let crate_kind: cargo_metadata::CrateType = target_kind_str.into();
+
+ let target = TargetBuilder::default()
+ .name(self.target_name.as_deref().unwrap_or_default())
+ .kind(vec![target_kind_parsed])
+ .crate_types(vec![crate_kind])
+ .required_features(Vec::<String>::new())
+ .src_path(self.target_src_path.as_deref().unwrap_or_default())
+ .edition(Edition::E2021)
+ .doctest(false)
+ .test(false)
+ .doc(false)
+ .build()
+ .unwrap();
+
+ let diagnostic = self.build_diagnostic(
+ &self.message,
+ &self.lint_code,
+ &self.level,
+ &self.spans,
+ &self.attached_reports,
+ );
+
+ Message::CompilerMessage(
+ CompilerMessageBuilder::default()
+ .package_id(PackageId {
+ repr: self.package_id.clone().unwrap_or_else(|| "unknown".into()),
+ })
+ .target(target)
+ .message(diagnostic)
+ .build()
+ .unwrap(),
+ )
+ }
+
+ fn build_diagnostic(
+ &self,
+ message: &str,
+ code: &str,
+ level: &MlintLevel,
+ spans: &[LintSpan],
+ children: &[MlintReport],
+ ) -> cargo_metadata::diagnostic::Diagnostic {
+ cargo_metadata::diagnostic::DiagnosticBuilder::default()
+ .message(message)
+ .code(
+ DiagnosticCodeBuilder::default()
+ .code(code)
+ .explanation(None)
+ .build()
+ .unwrap(),
+ )
+ .level(match level {
+ MlintLevel::Error => CargoLevel::Error,
+ MlintLevel::Warning => CargoLevel::Warning,
+ MlintLevel::Note => CargoLevel::Note,
+ MlintLevel::Help => CargoLevel::Help,
+ })
+ .spans(
+ spans
+ .iter()
+ .map(|s| self.lint_span_to_diagnostic_span(s))
+ .collect::<Vec<_>>(),
+ )
+ .children(
+ children
+ .iter()
+ .map(|c| {
+ self.build_diagnostic(
+ &c.message,
+ &c.lint_code,
+ &c.level,
+ &c.spans,
+ &c.attached_reports,
+ )
+ })
+ .collect::<Vec<_>>(),
+ )
+ .rendered(None)
+ .build()
+ .unwrap()
+ }
+
+ fn lint_span_to_diagnostic_span(
+ &self,
+ span: &LintSpan,
+ ) -> cargo_metadata::diagnostic::DiagnosticSpan {
+ let byte_start = self.line_col_to_byte_offset(span.line_start, span.column_start);
+ let byte_end = self.line_col_to_byte_offset(span.line_end, span.column_end);
+
+ DiagnosticSpanBuilder::default()
+ .file_name(&self.file_name)
+ .byte_start(byte_start as u32)
+ .byte_end(byte_end as u32)
+ .line_start(span.line_start)
+ .line_end(span.line_end)
+ .column_start(span.column_start)
+ .column_end(span.column_end)
+ .is_primary(true)
+ .text(
+ span.text
+ .iter()
+ .map(|l| {
+ DiagnosticSpanLineBuilder::default()
+ .text(&l.text)
+ .highlight_start(l.highlight_start)
+ .highlight_end(l.highlight_end)
+ .build()
+ .unwrap()
+ })
+ .collect::<Vec<_>>(),
+ )
+ .label(span.label.clone())
+ .suggested_replacement(None::<String>)
+ .suggestion_applicability(None::<cargo_metadata::diagnostic::Applicability>)
+ .expansion(None)
+ .build()
+ .unwrap()
+ }
+}
+
+pack!(StateLintReports = Vec<MlintReport>);
+pack!(ResultLintReportsAnnotateSnippet = Vec<MlintReport>);
+pack!(ResultLintReportsJson = Vec<MlintReport>);
+
+#[chain]
+pub fn handle_state_lint_reports(reports: StateLintReports, using_json: &ResUsingJson) -> Next {
+ if using_json.using {
+ ResultLintReportsJson::new(reports.inner).to_render()
+ } else {
+ ResultLintReportsAnnotateSnippet::new(reports.inner).to_render()
+ }
+}
+
+#[renderer(buffer)]
+pub fn render_lint_reports(reports: ResultLintReportsAnnotateSnippet) {
+ for report in reports.inner {
+ r_eprintln!("{}", report.to_annotate_snippet_render());
+ }
+}
+
+#[renderer(buffer)]
+pub fn render_lint_reports_json(reports: ResultLintReportsJson) {
+ for report in reports.inner {
+ // DIRTY: Dispatch to the Message renderer using AnyOutput to obtain the render result and append it to the Buffer
+ r_append!(|| { crate::ThisProgram::render(AnyOutput::new(report.to_compiler_message())) });
+ }
+}