use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::fmt::Write as FmtWrite;
use std::io::{self, Write as IoWrite};
use std::path::{Path, PathBuf};
#[inline]
fn path_to_string(path: &std::path::PathBuf) -> String {
path.to_string_lossy().to_string()
}
pub use crate::clang::diagnostics::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorMode {
Auto,
Always,
Never,
}
impl ColorMode {
pub fn should_use_color(&self, is_tty: bool) -> bool {
match self {
ColorMode::Auto => is_tty,
ColorMode::Always => true,
ColorMode::Never => false,
}
}
}
impl fmt::Display for ColorMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ColorMode::Auto => write!(f, "auto"),
ColorMode::Always => write!(f, "always"),
ColorMode::Never => write!(f, "never"),
}
}
}
pub mod ansi {
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";
pub const CYAN: &str = "\x1b[36m";
pub const WHITE: &str = "\x1b[37m";
pub const BRIGHT_RED: &str = "\x1b[91m";
pub const BRIGHT_GREEN: &str = "\x1b[92m";
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
pub const BRIGHT_CYAN: &str = "\x1b[96m";
pub const BG_RED: &str = "\x1b[41m";
pub const BG_GREEN: &str = "\x1b[42m";
pub const BG_YELLOW: &str = "\x1b[43m";
}
#[derive(Debug, Clone)]
pub struct X86ColorScheme {
pub use_colors: bool,
pub error_color: &'static str,
pub warning_color: &'static str,
pub note_color: &'static str,
pub fixit_color: &'static str,
pub caret_color: &'static str,
pub source_color: &'static str,
pub filename_color: &'static str,
pub line_number_color: &'static str,
pub range_color: &'static str,
pub macro_color: &'static str,
pub include_color: &'static str,
}
impl X86ColorScheme {
pub fn clang_style(use_colors: bool) -> Self {
Self {
use_colors,
error_color: ansi::BRIGHT_RED,
warning_color: ansi::BRIGHT_MAGENTA,
note_color: ansi::BRIGHT_CYAN,
fixit_color: ansi::BRIGHT_GREEN,
caret_color: ansi::BRIGHT_GREEN,
source_color: ansi::RESET,
filename_color: ansi::BOLD,
line_number_color: ansi::DIM,
range_color: ansi::BRIGHT_GREEN,
macro_color: ansi::DIM,
include_color: ansi::DIM,
}
}
pub fn monochrome() -> Self {
Self {
use_colors: false,
error_color: "",
warning_color: "",
note_color: "",
fixit_color: "",
caret_color: "",
source_color: "",
filename_color: "",
line_number_color: "",
range_color: "",
macro_color: "",
include_color: "",
}
}
pub fn error(&self, text: &str) -> String {
self.colorize(self.error_color, text)
}
pub fn warning(&self, text: &str) -> String {
self.colorize(self.warning_color, text)
}
pub fn note(&self, text: &str) -> String {
self.colorize(self.note_color, text)
}
pub fn fixit(&self, text: &str) -> String {
self.colorize(self.fixit_color, text)
}
pub fn caret(&self, text: &str) -> String {
self.colorize(self.caret_color, text)
}
pub fn filename(&self, text: impl AsRef<std::path::Path>) -> String {
self.colorize(self.filename_color, &text.as_ref().display().to_string())
}
pub fn line_number(&self, text: &str) -> String {
self.colorize(self.line_number_color, text)
}
pub fn range_highlight(&self, text: &str) -> String {
self.colorize(self.range_color, text)
}
pub fn macro_bt(&self, text: &str) -> String {
self.colorize(self.macro_color, text)
}
pub fn include_st(&self, text: &str) -> String {
self.colorize(self.include_color, text)
}
fn colorize(&self, color: &'static str, text: &str) -> String {
if self.use_colors && !color.is_empty() {
format!("{}{}{}", color, text, ansi::RESET)
} else {
text.to_string()
}
}
}
impl Default for X86ColorScheme {
fn default() -> Self {
Self::clang_style(false)
}
}
#[derive(Debug, Clone)]
pub struct X86DiagnosticOptions {
pub color_mode: ColorMode,
pub show_column_numbers: bool,
pub show_option_names: bool,
pub show_source_ranges: bool,
pub show_carets: bool,
pub show_fixits: bool,
pub tab_stop: u32,
pub message_length_limit: usize,
pub show_categories: bool,
pub context_lines_before: u32,
pub context_lines_after: u32,
pub show_macro_backtrace: bool,
pub macro_backtrace_limit: u32,
pub show_include_stack: bool,
pub include_stack_limit: u32,
pub show_template_backtrace: bool,
pub template_backtrace_limit: u32,
pub fixit_colors: bool,
pub is_tty: bool,
}
impl X86DiagnosticOptions {
pub fn for_terminal() -> Self {
Self {
color_mode: ColorMode::Auto,
show_column_numbers: true,
show_option_names: true,
show_source_ranges: true,
show_carets: true,
show_fixits: true,
tab_stop: 8,
message_length_limit: 0,
show_categories: false,
context_lines_before: 1,
context_lines_after: 1,
show_macro_backtrace: true,
macro_backtrace_limit: 10,
show_include_stack: true,
include_stack_limit: 10,
show_template_backtrace: true,
template_backtrace_limit: 10,
fixit_colors: true,
is_tty: false,
}
}
pub fn for_ide() -> Self {
Self {
color_mode: ColorMode::Never,
show_column_numbers: true,
show_option_names: false,
show_source_ranges: false,
show_carets: false,
show_fixits: false,
tab_stop: 8,
message_length_limit: 0,
show_categories: false,
context_lines_before: 0,
context_lines_after: 0,
show_macro_backtrace: false,
macro_backtrace_limit: 0,
show_include_stack: false,
include_stack_limit: 0,
show_template_backtrace: false,
template_backtrace_limit: 0,
fixit_colors: false,
is_tty: false,
}
}
pub fn for_machine() -> Self {
Self {
color_mode: ColorMode::Never,
show_column_numbers: true,
show_option_names: false,
show_source_ranges: false,
show_carets: false,
show_fixits: false,
tab_stop: 8,
message_length_limit: 0,
show_categories: false,
context_lines_before: 0,
context_lines_after: 0,
show_macro_backtrace: false,
macro_backtrace_limit: 0,
show_include_stack: false,
include_stack_limit: 0,
show_template_backtrace: false,
template_backtrace_limit: 0,
fixit_colors: false,
is_tty: false,
}
}
pub fn use_colors(&self) -> bool {
self.color_mode.should_use_color(self.is_tty)
}
pub fn color_scheme(&self) -> X86ColorScheme {
X86ColorScheme::clang_style(self.use_colors())
}
}
impl Default for X86DiagnosticOptions {
fn default() -> Self {
Self::for_terminal()
}
}
#[derive(Debug, Clone)]
pub struct X86DiagnosticFilter {
pub min_severity: Option<DiagSeverity>,
pub file_filter: Option<Vec<PathBuf>>,
pub file_exclude: Option<Vec<PathBuf>>,
pub category_filter: Option<Vec<String>>,
pub category_exclude: Option<Vec<String>>,
pub id_filter: Option<HashSet<String>>,
pub line_range: Option<(u32, u32)>,
pub suppress_system_headers: bool,
pub error_limit: Option<u32>,
pub disabled_warnings: HashSet<String>,
pub message_suppress_patterns: Vec<String>,
pub only_with_fixits: bool,
pub deduplicate: bool,
pub dedup_limit: u32,
}
impl X86DiagnosticFilter {
pub fn new() -> Self {
Self {
min_severity: None,
file_filter: None,
file_exclude: None,
category_filter: None,
category_exclude: None,
id_filter: None,
line_range: None,
suppress_system_headers: false,
error_limit: None,
disabled_warnings: HashSet::new(),
message_suppress_patterns: Vec::new(),
only_with_fixits: false,
deduplicate: false,
dedup_limit: 5,
}
}
pub fn errors_only() -> Self {
Self {
min_severity: Some(DiagSeverity::Error),
..Default::default()
}
}
pub fn no_system_headers() -> Self {
Self {
suppress_system_headers: true,
..Default::default()
}
}
pub fn min_severity(mut self, severity: DiagSeverity) -> Self {
self.min_severity = Some(severity);
self
}
pub fn include_file(mut self, path: PathBuf) -> Self {
self.file_filter.get_or_insert_with(Vec::new).push(path);
self
}
pub fn exclude_file(mut self, path: PathBuf) -> Self {
self.file_exclude.get_or_insert_with(Vec::new).push(path);
self
}
pub fn include_category(mut self, category: &str) -> Self {
self.category_filter
.get_or_insert_with(Vec::new)
.push(category.to_string());
self
}
pub fn exclude_category(mut self, category: &str) -> Self {
self.category_exclude
.get_or_insert_with(Vec::new)
.push(category.to_string());
self
}
pub fn line_range(mut self, start: u32, end: u32) -> Self {
self.line_range = Some((start, end));
self
}
pub fn error_limit(mut self, limit: u32) -> Self {
self.error_limit = Some(limit);
self
}
pub fn disable_warning(mut self, flag: &str) -> Self {
self.disabled_warnings.insert(flag.to_string());
self
}
pub fn deduplicate(mut self, limit: u32) -> Self {
self.deduplicate = true;
self.dedup_limit = limit;
self
}
pub fn only_with_fixits(mut self) -> Self {
self.only_with_fixits = true;
self
}
pub fn should_show(
&self,
diag: &StructuredDiagnostic,
system_headers: &HashSet<PathBuf>,
) -> bool {
if let Some(min_sev) = &self.min_severity {
if !Self::severity_meets_min(&diag.severity, min_sev) {
return false;
}
}
if self.suppress_system_headers {
let file_path = &diag.location.file;
if system_headers.contains(file_path) {
return false;
}
}
if let Some(ref filter) = self.file_filter {
if !filter.iter().any(|f| diag.location.file.ends_with(f)) {
return false;
}
}
if let Some(ref exclude) = self.file_exclude {
if exclude.iter().any(|f| diag.location.file.ends_with(f)) {
return false;
}
}
if let Some(ref filter) = self.category_filter {
if !filter.iter().any(|c| diag.category.contains(c.as_str())) {
return false;
}
}
if let Some(ref exclude) = self.category_exclude {
if exclude.iter().any(|c| diag.category.contains(c.as_str())) {
return false;
}
}
if let Some(ref ids) = self.id_filter {
let diag_id = format!("{:?}", diag.id);
if !ids.contains(&diag_id) {
return false;
}
}
if let Some((start, end)) = self.line_range {
if diag.location.line < start || diag.location.line > end {
return false;
}
}
if let Some(ref flag) = diag.flag_name {
if self.disabled_warnings.contains(flag) {
return false;
}
}
if self.only_with_fixits && diag.fixits.is_empty() {
return false;
}
for pattern in &self.message_suppress_patterns {
if diag.message.contains(pattern) {
return false;
}
}
true
}
fn severity_meets_min(diag_sev: &DiagSeverity, min_sev: &DiagSeverity) -> bool {
use DiagSeverity::*;
let rank = |s: &DiagSeverity| -> u8 {
match s {
Ignored => 0,
Remark => 1,
Note => 2,
Warning => 3,
Error => 4,
Fatal => 5,
}
};
rank(diag_sev) >= rank(min_sev)
}
}
impl Default for X86DiagnosticFilter {
fn default() -> Self {
Self::new()
}
}
pub fn unicode_column_width(text: &str, tab_stop: u32) -> usize {
let mut width: usize = 0;
for ch in text.chars() {
if ch == '\t' {
width = ((width / tab_stop as usize) + 1) * tab_stop as usize;
} else if ch == '\n' || ch == '\r' {
break;
} else if ch.is_ascii() {
width += 1;
} else {
width += unicode_char_width(ch);
}
}
width
}
fn unicode_char_width(ch: char) -> usize {
if ch == '\u{200B}'
|| ch == '\u{200C}'
|| ch == '\u{200D}'
|| ch == '\u{FEFF}'
|| ch == '\u{200E}'
|| ch == '\u{200F}'
{
return 0;
}
if ('\u{0300}'..='\u{036F}').contains(&ch)
|| ('\u{1AB0}'..='\u{1AFF}').contains(&ch)
|| ('\u{1DC0}'..='\u{1DFF}').contains(&ch)
|| ('\u{20D0}'..='\u{20FF}').contains(&ch)
|| ('\u{FE20}'..='\u{FE2F}').contains(&ch)
{
return 0;
}
if ('\u{1100}'..='\u{115F}').contains(&ch) || ('\u{2329}'..='\u{232A}').contains(&ch) || ('\u{2E80}'..='\u{303E}').contains(&ch) || ('\u{3040}'..='\u{33BF}').contains(&ch) || ('\u{3400}'..='\u{4DBF}').contains(&ch) || ('\u{4E00}'..='\u{A4CF}').contains(&ch) || ('\u{AC00}'..='\u{D7A3}').contains(&ch) || ('\u{F900}'..='\u{FAFF}').contains(&ch) || ('\u{FE10}'..='\u{FE19}').contains(&ch) || ('\u{FE30}'..='\u{FE6F}').contains(&ch) || ('\u{FF01}'..='\u{FF60}').contains(&ch) || ('\u{FFE0}'..='\u{FFE6}').contains(&ch) || ('\u{1F300}'..='\u{1F64F}').contains(&ch) || ('\u{1F900}'..='\u{1F9FF}').contains(&ch) || ('\u{20000}'..='\u{2FFFD}').contains(&ch) || ('\u{30000}'..='\u{3FFFD}').contains(&ch)
{
return 2;
}
1
}
pub fn column_to_byte_offset(line: &str, column: u32, tab_stop: u32) -> usize {
let mut current_col: usize = 0;
for (idx, ch) in line.char_indices() {
if current_col >= column as usize {
return idx;
}
if ch == '\t' {
current_col = ((current_col / tab_stop as usize) + 1) * tab_stop as usize;
} else if ch == '\n' || ch == '\r' {
break;
} else {
current_col += unicode_char_width(ch);
}
}
line.len()
}
pub fn expand_tabs(text: &str, tab_stop: u32) -> String {
if tab_stop == 0 {
return text.replace('\t', "");
}
let mut result = String::with_capacity(text.len());
for line in text.lines() {
let mut col: usize = 0;
for ch in line.chars() {
if ch == '\t' {
let spaces = tab_stop as usize - (col % tab_stop as usize);
result.push_str(&" ".repeat(spaces));
col += spaces;
} else {
result.push(ch);
col += unicode_char_width(ch);
}
}
result.push('\n');
}
if !text.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
#[derive(Debug, Clone)]
pub struct SourceLineCache {
cache: HashMap<PathBuf, Vec<String>>,
tab_stop: u32,
}
impl SourceLineCache {
pub fn new(tab_stop: u32) -> Self {
Self {
cache: HashMap::new(),
tab_stop,
}
}
pub fn get_line(
&mut self,
source_manager: &ClangSourceManager,
file: &Path,
line: u32,
) -> Option<String> {
if let Some(lines) = self.cache.get(file) {
if (line as usize) <= lines.len() && line > 0 {
return Some(lines[(line - 1) as usize].clone());
}
return None;
}
if let Some(src_file) = source_manager.get_file(file) {
let raw_lines: Vec<String> = src_file.content.lines().map(|l| l.to_string()).collect();
let expanded: Vec<String> = raw_lines
.iter()
.map(|l| expand_tabs(l, self.tab_stop))
.collect();
self.cache.insert(file.to_path_buf(), expanded.clone());
if (line as usize) <= expanded.len() && line > 0 {
return Some(expanded[(line - 1) as usize].clone());
}
}
None
}
pub fn get_lines(
&mut self,
source_manager: &ClangSourceManager,
file: &Path,
start_line: u32,
end_line: u32,
) -> Vec<(u32, String)> {
let mut lines = Vec::new();
for line_num in start_line..=end_line {
if let Some(content) = self.get_line(source_manager, file, line_num) {
lines.push((line_num, content));
}
}
lines
}
pub fn clear(&mut self) {
self.cache.clear();
}
}
#[derive(Debug, Clone)]
pub struct X86TermRenderer {
pub color_scheme: X86ColorScheme,
pub options: X86DiagnosticOptions,
}
impl X86TermRenderer {
pub fn new(color_scheme: X86ColorScheme, options: X86DiagnosticOptions) -> Self {
Self {
color_scheme,
options,
}
}
pub fn render(
&self,
diag: &StructuredDiagnostic,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let sev_str = diag.severity.as_str();
let color = self.color_for_severity(&diag.severity);
if diag.location.is_valid {
write!(
output,
"{}{} {}: {}\n",
self.color_scheme
.filename(diag.location.file.to_string_lossy().as_ref()),
if self.options.show_column_numbers {
format!(":{}:{}", diag.location.line, diag.location.column)
} else {
format!(":{}", diag.location.line)
},
color(sev_str),
diag.message
)
.unwrap();
} else {
writeln!(output, "{}{}: {}", color(sev_str), " ", diag.message).unwrap();
}
if self.options.show_option_names {
if let Some(ref flag) = diag.flag_name {
writeln!(output, "{}", flag).unwrap();
}
}
if self.options.show_categories {
writeln!(output, " [{}]", diag.category).unwrap();
}
if self.options.show_carets && diag.location.is_valid {
let snippet = self.render_source_snippet(diag, source_manager);
if !snippet.is_empty() {
output.push_str(&snippet);
}
}
if self.options.show_source_ranges && !diag.ranges.is_empty() {
let range_text = self.render_ranges(diag, source_manager);
if !range_text.is_empty() {
output.push_str(&range_text);
}
}
if self.options.show_fixits && !diag.fixits.is_empty() {
let fixit_text = self.render_fixits(diag, source_manager);
if !fixit_text.is_empty() {
output.push_str(&fixit_text);
}
}
for note in &diag.notes {
let note_text = self.render_note(note, source_manager);
output.push_str(¬e_text);
}
output
}
fn render_source_snippet(
&self,
diag: &StructuredDiagnostic,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let mut line_cache = SourceLineCache::new(self.options.tab_stop);
let start_line = if diag.location.line > self.options.context_lines_before {
diag.location.line - self.options.context_lines_before
} else {
1u32
};
let end_line = diag.location.line + self.options.context_lines_after;
let max_line = end_line;
let line_num_width = max_line.to_string().len().max(4);
let source_line = line_cache.get_line(
source_manager,
diag.location.file.as_path(),
diag.location.line,
);
for line_num in start_line..=end_line {
if let Some(line) = &source_line {
if line_num >= start_line && line_num <= end_line {
}
}
if let Some(line_content) =
line_cache.get_line(source_manager, diag.location.file.as_path(), line_num)
{
let is_diag_line = line_num == diag.location.line;
let marker = if is_diag_line { ">" } else { " " };
writeln!(
output,
"{} {} | {}",
self.color_scheme.line_number(&format!(
"{}{:>width$}",
marker,
line_num,
width = line_num_width
)),
self.color_scheme.line_number("|"),
&line_content
)
.unwrap();
if is_diag_line {
let caret_line =
self.build_caret_line(&line_content, diag.location.column, &diag.ranges);
writeln!(
output,
" {:>width$} | {}",
"",
self.color_scheme.caret(&caret_line),
width = line_num_width
)
.unwrap();
}
}
}
output
}
fn build_caret_line(&self, source_line: &str, column: u32, ranges: &[SourceRange]) -> String {
let tab_stop = self.options.tab_stop;
let col = unicode_column_width_before(source_line, column, tab_stop);
let mut caret = vec![' '; col.max(1)];
if col > 0 {
caret[col - 1] = '^';
} else {
caret.push('^');
}
for range in ranges {
if range.begin.line == range.end.line && range.begin.column != range.end.column {
let start_col = unicode_column_width_before(
source_line,
range.begin.column.min(range.end.column),
tab_stop,
);
let end_col = unicode_column_width_before(
source_line,
range.begin.column.max(range.end.column),
tab_stop,
);
let begin = start_col.max(col);
let end = end_col.min(source_line.len());
for i in begin..end {
if i < caret.len() {
if caret[i] == ' ' || caret[i] == '\0' {
caret[i] = '~';
}
}
}
if end > caret.len() {
caret.resize(end, '~');
for i in begin..end {
if caret[i] == '\0' {
caret[i] = '~';
}
}
}
}
}
let result: String = caret.into_iter().collect();
result.trim_end().to_string()
}
fn render_ranges(
&self,
diag: &StructuredDiagnostic,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let mut line_cache = SourceLineCache::new(self.options.tab_stop);
for range in &diag.ranges {
if range.is_valid() && range.begin.line == range.end.line {
if let Some(line) =
line_cache.get_line(source_manager, &range.begin.file, range.begin.line)
{
let start_col = unicode_column_width_before(
&line,
range.begin.column.min(range.end.column),
self.options.tab_stop,
);
let end_col = unicode_column_width_before(
&line,
range.begin.column.max(range.end.column),
self.options.tab_stop,
);
let len = end_col.saturating_sub(start_col).max(1);
let line_num_width = range.begin.line.to_string().len().max(4);
writeln!(
output,
" {:>width$} | {}{}",
"",
" ".repeat(start_col),
self.color_scheme.range_highlight(&"~".repeat(len)),
width = line_num_width
)
.unwrap();
}
}
}
output
}
fn render_fixits(
&self,
diag: &StructuredDiagnostic,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
for fixit in &diag.fixits {
let loc = &fixit.remove_range.begin;
if fixit.is_insertion {
writeln!(
output,
"{} {}",
self.color_scheme.fixit(&format!(
"fix-it:\"{}:{}-{}\":",
loc.file.display(),
loc.line,
loc.column
)),
self.color_scheme
.fixit(&format!("insert \"{}\"", fixit.code))
)
.unwrap();
} else if fixit.code.is_empty() {
writeln!(
output,
"{} {}",
self.color_scheme.fixit(&format!(
"fix-it:\"{}:{}-{}-{}:{}\":",
loc.file.display(),
loc.line,
loc.column,
fixit.remove_range.end.line,
fixit.remove_range.end.column
)),
self.color_scheme.fixit("remove")
)
.unwrap();
} else {
writeln!(
output,
"{} {}",
self.color_scheme.fixit(&format!(
"fix-it:\"{}:{}-{}-{}:{}\":",
loc.file.display(),
loc.line,
loc.column,
fixit.remove_range.end.line,
fixit.remove_range.end.column
)),
self.color_scheme
.fixit(&format!("replace with \"{}\"", fixit.code))
)
.unwrap();
}
if self.options.fixit_colors
&& !fixit.is_insertion
&& !fixit.code.is_empty()
&& fixit.remove_range.is_valid()
{
let inline = self.render_inline_fixit(fixit, source_manager);
if !inline.is_empty() {
output.push_str(&inline);
}
}
}
output
}
fn render_inline_fixit(
&self,
fixit: &FixItHint,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let range = &fixit.remove_range;
let loc = &range.begin;
if let Some(src_file) = source_manager.get_file(&loc.file) {
let original = src_file.get_range_text(
range.begin.line,
range.begin.column,
range.end.line,
range.end.column,
);
if let Some(orig) = original {
writeln!(
output,
" {}{}",
ansi::RED,
format!("- {}", orig.replace('\n', "\n - "))
)
.unwrap();
writeln!(
output,
" {}{}",
ansi::GREEN,
format!("+ {}", fixit.code.replace('\n', "\n + "))
)
.unwrap();
if self.color_scheme.use_colors {
output.push_str(ansi::RESET);
}
}
}
output
}
fn render_note(&self, note: &DiagnosticNote, source_manager: &ClangSourceManager) -> String {
let mut output = String::new();
if note.location.is_valid {
writeln!(
output,
"{}{} {}: {}",
self.color_scheme
.filename(note.location.file.to_string_lossy().as_ref()),
if self.options.show_column_numbers {
format!(":{}:{}", note.location.line, note.location.column)
} else {
format!(":{}", note.location.line)
},
self.color_scheme.note("note"),
note.message
)
.unwrap();
} else {
writeln!(
output,
"{}{}: {}",
self.color_scheme.note("note"),
" ",
note.message
)
.unwrap();
}
if self.options.show_carets && note.location.is_valid {
let mut line_cache = SourceLineCache::new(self.options.tab_stop);
if let Some(line) = line_cache.get_line(
source_manager,
note.location.file.as_path(),
note.location.line,
) {
let line_num_width = note.location.line.to_string().len().max(4);
writeln!(
output,
" {:>width$} | {}",
note.location.line,
line,
width = line_num_width
)
.unwrap();
let caret_col =
unicode_column_width_before(&line, note.location.column, self.options.tab_stop);
let caret: String = (0..caret_col.max(1))
.map(|i| if i + 1 == caret_col.max(1) { '^' } else { ' ' })
.collect();
writeln!(
output,
" {:>width$} | {}",
"",
self.color_scheme.note(&caret),
width = line_num_width
)
.unwrap();
}
}
output
}
fn color_for_severity(&self, severity: &DiagSeverity) -> Box<dyn Fn(&str) -> String + '_> {
match severity {
DiagSeverity::Error | DiagSeverity::Fatal => {
Box::new(|s: &str| self.color_scheme.error(s))
}
DiagSeverity::Warning => Box::new(|s: &str| self.color_scheme.warning(s)),
DiagSeverity::Note => Box::new(|s: &str| self.color_scheme.note(s)),
DiagSeverity::Remark => Box::new(|s: &str| self.color_scheme.note(s)),
DiagSeverity::Ignored => Box::new(|s: &str| s.to_string()),
}
}
}
fn unicode_column_width_before(line: &str, column: u32, tab_stop: u32) -> usize {
if column == 0 {
return 0;
}
let mut current_col: usize = 0;
let mut width: usize = 0;
for ch in line.chars() {
if current_col + 1 >= column as usize {
break;
}
if ch == '\t' {
let tab_width = tab_stop as usize - (width % tab_stop as usize);
width += tab_width;
current_col += 1;
} else if ch == '\n' || ch == '\r' {
break;
} else {
let ch_width = unicode_char_width(ch);
width += ch_width;
current_col += 1;
}
}
width
}
#[derive(Debug, Clone, Default)]
pub struct X86JsonRenderer;
impl X86JsonRenderer {
pub fn new() -> Self {
Self
}
pub fn render_diagnostics(&self, diags: &[StructuredDiagnostic]) -> String {
let mut output = String::from("[\n");
for (i, diag) in diags.iter().enumerate() {
if i > 0 {
output.push_str(",\n");
}
output.push_str(&self.render_single(diag));
}
output.push_str("\n]\n");
output
}
pub fn render_single(&self, diag: &StructuredDiagnostic) -> String {
let mut json = String::from(" {\n");
json.push_str(&format!(
" \"severity\": {:?},\n",
diag.severity.as_str()
));
json.push_str(&format!(
" \"message\": {},\n",
json_escape(&diag.message)
));
if diag.location.is_valid {
json.push_str(&format!(
" \"file\": {},\n",
json_escape(&diag.location.file.to_string_lossy())
));
json.push_str(&format!(" \"line\": {},\n", diag.location.line));
json.push_str(&format!(" \"column\": {},\n", diag.location.column));
}
json.push_str(&format!(
" \"category\": {},\n",
json_escape(&diag.category)
));
json.push_str(&format!(" \"diagnostic_id\": {:?},\n", diag.id));
if let Some(ref flag) = diag.flag_name {
json.push_str(&format!(" \"flag\": {},\n", json_escape(flag)));
} else {
json.push_str(" \"flag\": null,\n");
}
json.push_str(" \"ranges\": [\n");
for (i, range) in diag.ranges.iter().enumerate() {
if i > 0 {
json.push_str(",\n");
}
json.push_str(&format!(
" {{ \"file\": {}, \"line\": {}, \"column\": {} }}",
json_escape(&range.begin.file.to_string_lossy()),
range.begin.line,
range.begin.column
));
}
json.push_str("\n ],\n");
json.push_str(" \"fixits\": [\n");
for (i, fixit) in diag.fixits.iter().enumerate() {
if i > 0 {
json.push_str(",\n");
}
let fixit_type = if fixit.is_insertion {
"insertion"
} else if fixit.code.is_empty() {
"removal"
} else {
"replacement"
};
json.push_str(&format!(
" {{ \"type\": \"{}\", \"file\": {}, \"line\": {}, \"column\": {}, \"code\": {} }}",
fixit_type,
json_escape(&fixit.remove_range.begin.file.to_string_lossy()),
fixit.remove_range.begin.line,
fixit.remove_range.begin.column,
json_escape(&fixit.code)
));
}
json.push_str("\n ],\n");
json.push_str(" \"notes\": [\n");
for (i, note) in diag.notes.iter().enumerate() {
if i > 0 {
json.push_str(",\n");
}
json.push_str(&format!(
" {{ \"message\": {}, \"file\": {}, \"line\": {}, \"column\": {} }}",
json_escape(¬e.message),
json_escape(¬e.location.file.to_string_lossy()),
note.location.line,
note.location.column
));
}
json.push_str("\n ]\n");
json.push_str(" }");
json
}
}
fn json_escape(s: &str) -> String {
let mut escaped = String::with_capacity(s.len() + 2);
escaped.push('"');
for ch in s.chars() {
match ch {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
'\x08' => escaped.push_str("\\b"),
'\x0c' => escaped.push_str("\\f"),
c if c.is_control() => {
escaped.push_str(&format!("\\u{:04x}", c as u32));
}
c => escaped.push(c),
}
}
escaped.push('"');
escaped
}
#[derive(Debug, Clone)]
pub struct X86SarifRenderer {
pub tool_name: String,
pub tool_version: String,
pub tool_info_uri: String,
}
impl X86SarifRenderer {
pub fn new() -> Self {
Self {
tool_name: "clang-x86".to_string(),
tool_version: "18.0.0".to_string(),
tool_info_uri: "https://clang.llvm.org/".to_string(),
}
}
pub fn with_tool_info(tool_name: &str, tool_version: &str, info_uri: &str) -> Self {
Self {
tool_name: tool_name.to_string(),
tool_version: tool_version.to_string(),
tool_info_uri: info_uri.to_string(),
}
}
pub fn render_log(&self, diags: &[StructuredDiagnostic], source_files: &[&str]) -> String {
let mut sarif = String::new();
sarif.push_str("{\n");
sarif.push_str(" \"$schema\": \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json\",\n");
sarif.push_str(" \"version\": \"2.1.0\",\n");
sarif.push_str(" \"runs\": [\n");
sarif.push_str(" {\n");
sarif.push_str(" \"tool\": {\n");
sarif.push_str(" \"driver\": {\n");
sarif.push_str(&format!(
" \"name\": {},\n",
json_escape(&self.tool_name)
));
sarif.push_str(&format!(
" \"version\": {},\n",
json_escape(&self.tool_version)
));
sarif.push_str(&format!(
" \"informationUri\": {},\n",
json_escape(&self.tool_info_uri)
));
sarif.push_str(" \"rules\": [\n");
sarif.push_str(&self.render_rules(diags));
sarif.push_str("\n ]\n");
sarif.push_str(" }\n");
sarif.push_str(" },\n");
sarif.push_str(" \"artifacts\": [\n");
for (i, file) in source_files.iter().enumerate() {
if i > 0 {
sarif.push_str(",\n");
}
sarif.push_str(&format!(
" {{\n \"location\": {{\n \"uri\": {}\n }}\n }}",
json_escape(file)
));
}
sarif.push_str("\n ],\n");
sarif.push_str(" \"results\": [\n");
for (i, diag) in diags.iter().enumerate() {
if i > 0 {
sarif.push_str(",\n");
}
sarif.push_str(&self.render_result(diag, i));
}
sarif.push_str("\n ]\n");
sarif.push_str(" }\n");
sarif.push_str(" ]\n");
sarif.push_str("}\n");
sarif
}
fn render_rules(&self, diags: &[StructuredDiagnostic]) -> String {
let mut rules = String::new();
let mut seen_ids: HashSet<String> = HashSet::new();
for (i, diag) in diags.iter().enumerate() {
let rule_id = format!("{:?}", diag.id);
if seen_ids.contains(&rule_id) {
continue;
}
seen_ids.insert(rule_id.clone());
if !rules.is_empty() {
rules.push_str(",\n");
}
let severity = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => "error",
DiagSeverity::Warning => "warning",
DiagSeverity::Note => "note",
DiagSeverity::Remark => "note",
DiagSeverity::Ignored => "none",
};
rules.push_str(&format!(
" {{\n \"id\": {},\n \"shortDescription\": {{\n \"text\": {}\n }},\n \"defaultConfiguration\": {{\n \"level\": \"{}\"\n }}\n }}",
json_escape(&rule_id),
json_escape(&diag.message),
severity
));
}
rules
}
fn render_result(&self, diag: &StructuredDiagnostic, index: usize) -> String {
let mut result = String::new();
let rule_id = format!("{:?}", diag.id);
let level = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => "error",
DiagSeverity::Warning => "warning",
DiagSeverity::Note | DiagSeverity::Remark => "note",
DiagSeverity::Ignored => "none",
};
result.push_str(" {\n");
result.push_str(&format!(
" \"ruleId\": {},\n",
json_escape(&rule_id)
));
result.push_str(&format!(" \"level\": \"{}\",\n", level));
result.push_str(&format!(
" \"message\": {{\n \"text\": {}\n }},\n",
json_escape(&diag.message)
));
if diag.location.is_valid {
result.push_str(" \"locations\": [\n");
result.push_str(" {\n");
result.push_str(" \"physicalLocation\": {\n");
result.push_str(" \"artifactLocation\": {\n");
result.push_str(&format!(
" \"uri\": {}\n",
json_escape(&diag.location.file.to_string_lossy())
));
result.push_str(" },\n");
result.push_str(" \"region\": {\n");
result.push_str(&format!(
" \"startLine\": {},\n",
diag.location.line
));
result.push_str(&format!(
" \"startColumn\": {}\n",
diag.location.column
));
result.push_str(" }\n");
result.push_str(" }\n");
result.push_str(" }\n");
result.push_str(" ],\n");
} else {
result.push_str(" \"locations\": [],\n");
}
if !diag.notes.is_empty() {
result.push_str(" \"relatedLocations\": [\n");
for (j, note) in diag.notes.iter().enumerate() {
if j > 0 {
result.push_str(",\n");
}
result.push_str(" {\n");
result.push_str(&format!(
" \"message\": {{\n \"text\": {}\n }},\n",
json_escape(¬e.message)
));
if note.location.is_valid {
result.push_str(" \"physicalLocation\": {\n");
result.push_str(" \"artifactLocation\": {\n");
result.push_str(&format!(
" \"uri\": {}\n",
json_escape(¬e.location.file.to_string_lossy())
));
result.push_str(" },\n");
result.push_str(" \"region\": {\n");
result.push_str(&format!(
" \"startLine\": {},\n",
note.location.line
));
result.push_str(&format!(
" \"startColumn\": {}\n",
note.location.column
));
result.push_str(" }\n");
result.push_str(" }\n");
}
result.push_str(" }");
}
result.push_str("\n ],\n");
}
if !diag.fixits.is_empty() {
result.push_str(" \"fixes\": [\n");
for (j, fixit) in diag.fixits.iter().enumerate() {
if j > 0 {
result.push_str(",\n");
}
result.push_str(" {\n");
result.push_str(" \"description\": {\n");
result.push_str(&format!(
" \"text\": {}\n",
json_escape(&format!("Apply fix-it: {}", fixit.code))
));
result.push_str(" },\n");
result.push_str(" \"artifactChanges\": [\n");
result.push_str(" {\n");
result.push_str(" \"artifactLocation\": {\n");
result.push_str(&format!(
" \"uri\": {}\n",
json_escape(&fixit.remove_range.begin.file.to_string_lossy())
));
result.push_str(" },\n");
result.push_str(" \"replacements\": [\n");
result.push_str(" {\n");
result.push_str(" \"deletedRegion\": {\n");
result.push_str(&format!(
" \"startLine\": {},\n",
fixit.remove_range.begin.line
));
result.push_str(&format!(
" \"startColumn\": {},\n",
fixit.remove_range.begin.column
));
if fixit.remove_range.begin.line != fixit.remove_range.end.line
|| fixit.remove_range.begin.column != fixit.remove_range.end.column
{
result.push_str(&format!(
" \"endLine\": {},\n",
fixit.remove_range.end.line
));
result.push_str(&format!(
" \"endColumn\": {}\n",
fixit.remove_range.end.column
));
} else {
result.push_str(&format!(
" \"endColumn\": {}\n",
fixit.remove_range.end.column
));
}
result.push_str(" },\n");
result.push_str(" \"insertedContent\": {\n");
result.push_str(&format!(
" \"text\": {}\n",
json_escape(&fixit.code)
));
result.push_str(" }\n");
result.push_str(" }\n");
result.push_str(" ]\n");
result.push_str(" }\n");
result.push_str(" ]\n");
result.push_str(" }");
}
result.push_str("\n ]\n");
} else {
if result.ends_with(",\n") {
result.truncate(result.len() - 2);
result.push('\n');
}
}
result.push_str(" }");
result
}
}
impl Default for X86SarifRenderer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct X86XmlRenderer;
impl X86XmlRenderer {
pub fn new() -> Self {
Self
}
pub fn render(&self, diags: &[StructuredDiagnostic]) -> String {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<checkstyle version=\"4.3\">\n");
let mut file_groups: BTreeMap<String, Vec<&StructuredDiagnostic>> = BTreeMap::new();
for diag in diags {
if diag.location.is_valid {
file_groups
.entry(diag.location.file.to_string_lossy().to_string())
.or_default()
.push(diag);
}
}
for (file, file_diags) in &file_groups {
xml.push_str(&format!(" <file name=\"{}\">\n", xml_escape(file)));
for diag in file_diags {
let severity = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => "error",
DiagSeverity::Warning => "warning",
DiagSeverity::Note | DiagSeverity::Remark => "info",
DiagSeverity::Ignored => "info",
};
let source = diag
.flag_name
.as_ref()
.map(|s| s.as_str())
.unwrap_or("clang");
xml.push_str(&format!(
" <error line=\"{}\" column=\"{}\" severity=\"{}\" message=\"{}\" source=\"{}\" />\n",
diag.location.line,
diag.location.column,
severity,
xml_escape(&diag.message),
xml_escape(source),
));
}
xml.push_str(" </file>\n");
}
xml.push_str("</checkstyle>\n");
xml
}
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[derive(Debug, Clone)]
pub struct X86GccRenderer {
pub show_column: bool,
pub show_option_name: bool,
pub show_severity: bool,
}
impl X86GccRenderer {
pub fn new() -> Self {
Self {
show_column: true,
show_option_name: true,
show_severity: true,
}
}
pub fn render(&self, diag: &StructuredDiagnostic) -> String {
let severity_str = match diag.severity {
DiagSeverity::Error => "error",
DiagSeverity::Fatal => "fatal error",
DiagSeverity::Warning => "warning",
DiagSeverity::Note => "note",
DiagSeverity::Remark => "remark",
DiagSeverity::Ignored => "info",
};
let location = if diag.location.is_valid {
if self.show_column {
format!(
"{}:{}:{}:",
diag.location.file.display(),
diag.location.line,
diag.location.column
)
} else {
format!("{}:{}:", diag.location.file.display(), diag.location.line)
}
} else {
String::new()
};
let mut output = if self.show_severity {
format!("{} {}: {}", location, severity_str, diag.message)
} else {
format!("{} {}", location, diag.message)
};
if self.show_option_name {
if let Some(ref flag) = diag.flag_name {
output.push_str(&format!(" [{}]", flag));
}
}
output
}
pub fn render_all(&self, diags: &[StructuredDiagnostic]) -> String {
let mut output = String::new();
for diag in diags {
output.push_str(&self.render(diag));
output.push('\n');
for note in &diag.notes {
let note_line = self.render_note(note);
output.push_str(¬e_line);
output.push('\n');
}
}
output
}
fn render_note(&self, note: &DiagnosticNote) -> String {
if note.location.is_valid {
if self.show_column {
format!(
"{}:{}:{}: note: {}",
note.location.file.display(),
note.location.line,
note.location.column,
note.message
)
} else {
format!(
"{}:{}: note: {}",
note.location.file.display(),
note.location.line,
note.message
)
}
} else {
format!("note: {}", note.message)
}
}
}
impl Default for X86GccRenderer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86MsvcRenderer {
pub show_column: bool,
}
impl X86MsvcRenderer {
pub fn new() -> Self {
Self { show_column: true }
}
pub fn render(&self, diag: &StructuredDiagnostic) -> String {
let code = self.msvc_code_for_diag(diag);
let severity_label = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => "error",
DiagSeverity::Warning => "warning",
DiagSeverity::Note | DiagSeverity::Remark => "note",
DiagSeverity::Ignored => "info",
};
if diag.location.is_valid {
if self.show_column {
format!(
"{}({},{}): {} {}: {}",
diag.location.file.display(),
diag.location.line,
diag.location.column,
severity_label,
code,
diag.message
)
} else {
format!(
"{}({}): {} {}: {}",
diag.location.file.display(),
diag.location.line,
severity_label,
code,
diag.message
)
}
} else {
format!("{} {}: {}", severity_label, code, diag.message)
}
}
pub fn render_all(&self, diags: &[StructuredDiagnostic]) -> String {
let mut output = String::new();
for diag in diags {
output.push_str(&self.render(diag));
output.push('\n');
}
output
}
fn msvc_code_for_diag(&self, diag: &StructuredDiagnostic) -> String {
let prefix = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => 'C',
DiagSeverity::Warning => 'C',
DiagSeverity::Note => 'C',
DiagSeverity::Remark => 'C',
DiagSeverity::Ignored => 'C',
};
let id_str = format!("{:?}", diag.id);
let mut hash: u32 = 5381;
for b in id_str.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(b as u32);
}
let code_num = 2000 + (hash % 8000);
format!("{}{:04}", prefix, code_num)
}
}
impl Default for X86MsvcRenderer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct X86ViRenderer;
impl X86ViRenderer {
pub fn new() -> Self {
Self
}
pub fn render(&self, diag: &StructuredDiagnostic) -> String {
if diag.location.is_valid {
format!(
"{} +{} {}: {}",
diag.location.file.display(),
diag.location.line,
diag.location.column,
diag.message
)
} else {
diag.message.clone()
}
}
pub fn render_all(&self, diags: &[StructuredDiagnostic]) -> String {
let mut output = String::new();
for diag in diags {
output.push_str(&self.render(diag));
output.push('\n');
}
output
}
}
#[derive(Debug, Clone)]
pub struct X86FixItRenderer {
pub use_colors: bool,
pub tab_stop: u32,
}
impl X86FixItRenderer {
pub fn new(use_colors: bool, tab_stop: u32) -> Self {
Self {
use_colors,
tab_stop,
}
}
pub fn render_fixits(
&self,
fixits: &[FixItHint],
source_manager: &ClangSourceManager,
) -> String {
if fixits.is_empty() {
return String::new();
}
let mut output = String::new();
for (i, fixit) in fixits.iter().enumerate() {
if i > 0 {
output.push('\n');
}
output.push_str(&self.render_single_fixit(fixit, source_manager));
}
output
}
fn render_single_fixit(
&self,
fixit: &FixItHint,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let loc = &fixit.remove_range.begin;
let end = &fixit.remove_range.end;
if fixit.is_insertion {
output.push_str(&format!(
"fix-it:\"{}:{}-{}\": {{",
loc.file.display(),
loc.line,
loc.column
));
output.push_str(&format!("insert \"{}\"", fixit.code));
output.push_str("}\n");
} else if fixit.code.is_empty() {
output.push_str(&format!(
"fix-it:\"{}:{}-{}:{}-{}\": {{",
loc.file.display(),
loc.line,
loc.column,
end.line,
end.column
));
output.push_str("remove");
output.push_str("}\n");
} else {
output.push_str(&format!(
"fix-it:\"{}:{}-{}:{}-{}\": {{",
loc.file.display(),
loc.line,
loc.column,
end.line,
end.column
));
output.push_str(&format!("replace with \"{}\"", fixit.code));
output.push_str("}\n");
}
if !fixit.is_insertion && !fixit.code.is_empty() {
let inline = self.render_inline_diff(fixit, source_manager);
if !inline.is_empty() {
output.push_str(&inline);
}
}
output
}
fn render_inline_diff(&self, fixit: &FixItHint, source_manager: &ClangSourceManager) -> String {
let range = &fixit.remove_range;
let loc = &range.begin;
let mut output = String::new();
if let Some(src_file) = source_manager.get_file(&loc.file) {
let original = src_file.get_range_text(
range.begin.line,
range.begin.column,
range.end.line,
range.end.column,
);
if let Some(orig) = original {
let context_line =
self.get_context_before(&loc.file.to_string_lossy(), loc.line, source_manager);
if let Some(ctx) = context_line {
output.push_str(&format!(" {}\n", ctx));
}
for line in orig.lines() {
if self.use_colors {
output.push_str(&format!(" {}- {}{}\n", ansi::RED, line, ansi::RESET));
} else {
output.push_str(&format!(" - {}\n", line));
}
}
for line in fixit.code.lines() {
if self.use_colors {
output.push_str(&format!(" {}+ {}{}\n", ansi::GREEN, line, ansi::RESET));
} else {
output.push_str(&format!(" + {}\n", line));
}
}
let context_line = self.get_context_after(
&loc.file.to_string_lossy(),
range.end.line,
source_manager,
);
if let Some(ctx) = context_line {
output.push_str(&format!(" {}\n", ctx));
}
}
}
output
}
fn get_context_before(
&self,
file: &str,
line: u32,
source_manager: &ClangSourceManager,
) -> Option<String> {
if line > 1 {
source_manager.get_line(Path::new(file), line - 1)
} else {
None
}
}
fn get_context_after(
&self,
file: &str,
line: u32,
source_manager: &ClangSourceManager,
) -> Option<String> {
source_manager.get_line(Path::new(file), line + 1)
}
pub fn apply_fixits(
&self,
fixits: &[FixItHint],
source_manager: &ClangSourceManager,
) -> Result<HashMap<String, String>, String> {
let conflicts = self.detect_conflicts(fixits);
if !conflicts.is_empty() {
return Err(format!(
"Fix-it conflicts detected: {}",
conflicts.join("; ")
));
}
let mut file_changes: HashMap<String, Vec<(usize, usize, String)>> = HashMap::new();
for fixit in fixits {
let loc = &fixit.remove_range.begin;
let end = &fixit.remove_range.end;
if let Some(src_file) = source_manager.get_file(&loc.file) {
let start_offset = src_file
.get_line_offset(loc.line)
.map(|o| o + loc.column as usize - 1)
.unwrap_or(0);
let end_offset = if fixit.is_insertion {
start_offset
} else {
src_file
.get_line_offset(end.line)
.map(|o| o + end.column as usize - 1)
.unwrap_or(start_offset)
};
file_changes
.entry(loc.file.to_string_lossy().to_string())
.or_default()
.push((start_offset, end_offset, fixit.code.clone()));
}
}
let mut results: HashMap<String, String> = HashMap::new();
for (file, mut changes) in file_changes {
if let Some(src_file) = source_manager.get_file(Path::new(&file)) {
changes.sort_by_key(|(start, _, _)| *start);
changes.reverse();
let mut content = src_file.content.clone();
for (start, end, replacement) in &changes {
content.replace_range(*start..*end, replacement);
}
results.insert(file, content);
}
}
Ok(results)
}
pub fn detect_conflicts(&self, fixits: &[FixItHint]) -> Vec<String> {
let mut conflicts = Vec::new();
for i in 0..fixits.len() {
for j in (i + 1)..fixits.len() {
if self.fixits_overlap(&fixits[i], &fixits[j]) {
conflicts.push(format!(
"Fix-it {} and {} overlap in file '{}'",
i,
j,
fixits[i].remove_range.begin.file.display()
));
}
}
}
conflicts
}
fn fixits_overlap(&self, a: &FixItHint, b: &FixItHint) -> bool {
if a.remove_range.begin.file != b.remove_range.begin.file {
return false;
}
let a_start = (a.remove_range.begin.line, a.remove_range.begin.column);
let a_end = (a.remove_range.end.line, a.remove_range.end.column);
let b_start = (b.remove_range.begin.line, b.remove_range.begin.column);
let b_end = (b.remove_range.end.line, b.remove_range.end.column);
!(a_end < b_start || b_end < a_start)
}
}
impl Default for X86FixItRenderer {
fn default() -> Self {
Self::new(true, 8)
}
}
trait SourceFileExt {
fn get_line_offset(&self, line: u32) -> Option<usize>;
}
impl SourceFileExt for crate::clang::diagnostics::ClangSourceFile {
fn get_line_offset(&self, line: u32) -> Option<usize> {
if line == 0 || line as usize > self.line_offsets.len() {
return None;
}
Some(self.line_offsets[(line - 1) as usize])
}
}
#[derive(Debug, Clone)]
pub struct X86DiagGrouper {
pub dedup_limit: u32,
pub group_template_notes: bool,
pub group_macro_notes: bool,
seen: HashMap<DiagFingerprint, u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct DiagFingerprint {
id_display: String,
file: String,
line: u32,
column: u32,
message_hash: u64,
}
impl X86DiagGrouper {
pub fn new() -> Self {
Self {
dedup_limit: 5,
group_template_notes: true,
group_macro_notes: true,
seen: HashMap::new(),
}
}
pub fn with_dedup_limit(mut self, limit: u32) -> Self {
self.dedup_limit = limit;
self
}
pub fn process(&mut self, diags: Vec<StructuredDiagnostic>) -> Vec<ProcessedDiagnosticGroup> {
let mut groups: Vec<Vec<StructuredDiagnostic>> = Vec::new();
let mut current_group: Vec<StructuredDiagnostic> = Vec::new();
for diag in diags {
if diag.severity == DiagSeverity::Note
&& !current_group.is_empty()
&& self.should_group_with_previous(&diag, current_group.last().unwrap())
{
current_group.push(diag);
continue;
}
if !current_group.is_empty() {
groups.push(std::mem::take(&mut current_group));
}
current_group.push(diag);
}
if !current_group.is_empty() {
groups.push(current_group);
}
let mut result = Vec::new();
for group in groups {
let primary = &group[0];
let fingerprint = self.fingerprint(primary);
let count = self.seen.entry(fingerprint).or_insert(0);
*count += 1;
if *count <= self.dedup_limit {
result.push(ProcessedDiagnosticGroup {
primary: primary.clone(),
notes: group[1..].to_vec(),
occurrence_count: *count,
suppressed_count: 0,
});
} else if *count == self.dedup_limit + 1 {
if let Some(last) = result.last_mut() {
last.suppressed_count = *count - self.dedup_limit;
}
} else if let Some(last) = result.last_mut() {
last.suppressed_count = *count - self.dedup_limit;
}
}
self.seen.clear();
result
}
fn fingerprint(&self, diag: &StructuredDiagnostic) -> DiagFingerprint {
let mut hash: u64 = 5381;
for b in diag.message.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(b as u64);
}
DiagFingerprint {
id_display: format!("{:?}", diag.id),
file: diag.location.file.to_string_lossy().to_string(),
line: diag.location.line,
column: diag.location.column,
message_hash: hash,
}
}
fn should_group_with_previous(
&self,
note: &StructuredDiagnostic,
previous: &StructuredDiagnostic,
) -> bool {
if self.group_template_notes {
if note.message.contains("in instantiation of")
|| note.message.contains("required from")
|| note.message.contains("in expansion of")
{
return true;
}
}
if self.group_macro_notes {
if note.message.contains("expanded from macro")
|| note.message.contains("in definition of macro")
{
return true;
}
}
if note.message.contains("previous")
|| note.message.contains("declared here")
|| note.message.contains("defined here")
{
return true;
}
false
}
}
impl Default for X86DiagGrouper {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ProcessedDiagnosticGroup {
pub primary: StructuredDiagnostic,
pub notes: Vec<StructuredDiagnostic>,
pub occurrence_count: u32,
pub suppressed_count: u32,
}
#[derive(Debug, Clone)]
pub struct MacroExpansionStep {
pub macro_name: String,
pub definition_location: ClangSourceLocation,
pub expansion_location: ClangSourceLocation,
pub depth: u32,
pub source_context: Option<String>,
}
#[derive(Debug, Clone)]
pub struct X86MacroBacktrace {
pub max_depth: u32,
pub color_scheme: X86ColorScheme,
}
impl X86MacroBacktrace {
pub fn new(max_depth: u32, color_scheme: X86ColorScheme) -> Self {
Self {
max_depth,
color_scheme,
}
}
pub fn render(
&self,
steps: &[MacroExpansionStep],
source_manager: &ClangSourceManager,
) -> String {
if steps.is_empty() {
return String::new();
}
let mut output = String::new();
let display_steps: Vec<&MacroExpansionStep> =
steps.iter().take(self.max_depth as usize).collect();
for step in display_steps {
let indent = " ".repeat(step.depth as usize);
if step.depth == 0 {
output.push_str(&format!(
"{}in expansion of macro '{}'",
indent, step.macro_name
));
} else {
output.push_str(&format!(
"{}from expansion of macro '{}'",
indent, step.macro_name
));
}
if step.expansion_location.is_valid {
output.push_str(&format!(
" at {}:{}:{}",
step.expansion_location.file.display(),
step.expansion_location.line,
step.expansion_location.column
));
}
output.push('\n');
if step.definition_location.is_valid {
output.push_str(&format!(
"{} defined at {}:{}:{}\n",
indent,
step.definition_location.file.display(),
step.definition_location.line,
step.definition_location.column
));
}
if let Some(ref ctx) = step.source_context {
output.push_str(&format!("{} {}\n", indent, ctx));
}
}
if self.color_scheme.use_colors {
let colored: String = output
.lines()
.map(|line| self.color_scheme.macro_bt(line))
.collect::<Vec<_>>()
.join("\n");
if !output.ends_with('\n') {
colored
} else {
format!("{}\n", colored)
}
} else {
output
}
}
pub fn build_trace(
&self,
diag: &StructuredDiagnostic,
source_manager: &ClangSourceManager,
) -> Vec<MacroExpansionStep> {
let location = &diag.location;
if !location.is_valid || !location.is_macro_expansion {
return Vec::new();
}
let mut steps = Vec::new();
if let Some(ref spelling) = location.spelling_loc {
if let Some(ref expansion) = location.expansion_loc {
steps.push(MacroExpansionStep {
macro_name: self.extract_macro_name(diag),
definition_location: (**spelling).clone(),
expansion_location: (**expansion).clone(),
depth: 0,
source_context: self.get_source_context(expansion, source_manager),
});
}
}
steps
}
fn extract_macro_name(&self, diag: &StructuredDiagnostic) -> String {
for note in &diag.notes {
if note.message.contains("expanded from macro") {
if let Some(name) = note.message.strip_prefix("expanded from macro '") {
if let Some(end) = name.find('\'') {
return name[..end].to_string();
}
}
}
if note.message.contains("in definition of macro") {
if let Some(name) = note.message.strip_prefix("in definition of macro '") {
if let Some(end) = name.find('\'') {
return name[..end].to_string();
}
}
}
}
"<unknown macro>".to_string()
}
fn get_source_context(
&self,
loc: &ClangSourceLocation,
source_manager: &ClangSourceManager,
) -> Option<String> {
source_manager.get_line(&loc.file, loc.line)
}
}
#[derive(Debug, Clone)]
pub struct IncludeStackEntry {
pub file: String,
pub include_line: u32,
pub including_file: String,
pub depth: u32,
}
#[derive(Debug, Clone)]
pub struct X86IncludeStack {
pub max_depth: u32,
pub color_scheme: X86ColorScheme,
}
impl X86IncludeStack {
pub fn new(max_depth: u32, color_scheme: X86ColorScheme) -> Self {
Self {
max_depth,
color_scheme,
}
}
pub fn render(&self, stack: &[IncludeStackEntry]) -> String {
if stack.is_empty() {
return String::new();
}
let mut output = String::new();
let display_entries: Vec<&IncludeStackEntry> =
stack.iter().take(self.max_depth as usize).collect();
for entry in display_entries {
let indent = " ".repeat(entry.depth as usize);
if entry.depth == 0 {
output.push_str(&format!(
"{}In file included from {}:{}:\n",
indent, entry.including_file, entry.include_line
));
}
output.push_str(&format!("{}{}", indent, entry.file));
if entry.depth > 0 {
output.push_str(&format!(
" included from {}:{}",
entry.including_file, entry.include_line
));
}
output.push('\n');
}
if self.color_scheme.use_colors {
output
.lines()
.map(|line| self.color_scheme.include_st(line))
.collect::<Vec<_>>()
.join("\n")
+ "\n"
} else {
output
}
}
pub fn build_stack(
&self,
file: &str,
_source_manager: &ClangSourceManager,
_include_chain: &[(String, String, u32)],
) -> Vec<IncludeStackEntry> {
let mut stack = Vec::new();
for (depth, (included_file, including_file, line)) in _include_chain.iter().enumerate() {
stack.push(IncludeStackEntry {
file: included_file.clone(),
include_line: *line,
including_file: including_file.clone(),
depth: depth as u32,
});
}
if !_include_chain.is_empty() {
let last = _include_chain.last().unwrap();
stack.push(IncludeStackEntry {
file: file.to_string(),
include_line: 0,
including_file: last.0.clone(),
depth: _include_chain.len() as u32,
});
}
stack
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagnosticCompactor;
impl X86DiagnosticCompactor {
pub fn new() -> Self {
Self
}
pub fn compact(&self, diag: &StructuredDiagnostic) -> String {
let severity = diag.severity.as_str().to_uppercase();
let location = if diag.location.is_valid {
format!(
"{}:{}:{}",
&diag
.location
.file
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| diag.location.file.to_string_lossy().to_string()),
diag.location.line,
diag.location.column
)
} else {
String::from("<no-loc>")
};
let flag = diag
.flag_name
.as_ref()
.map(|f| format!(" [{}]", f))
.unwrap_or_default();
format!("[{}] {}: {}{}", severity, location, diag.message, flag)
}
pub fn compact_all(&self, diags: &[StructuredDiagnostic]) -> String {
diags
.iter()
.map(|d| self.compact(d))
.collect::<Vec<_>>()
.join("\n")
}
}
pub struct X86DiagRenderer {
pub options: X86DiagnosticOptions,
pub filter: X86DiagnosticFilter,
color_scheme: X86ColorScheme,
term_renderer: X86TermRenderer,
json_renderer: X86JsonRenderer,
sarif_renderer: X86SarifRenderer,
xml_renderer: X86XmlRenderer,
gcc_renderer: X86GccRenderer,
msvc_renderer: X86MsvcRenderer,
vi_renderer: X86ViRenderer,
fixit_renderer: X86FixItRenderer,
grouper: X86DiagGrouper,
macro_backtrace: X86MacroBacktrace,
include_stack: X86IncludeStack,
compactor: X86DiagnosticCompactor,
line_cache: SourceLineCache,
error_count: u32,
warning_count: u32,
system_headers: HashSet<PathBuf>,
source_manager: Option<ClangSourceManager>,
}
impl X86DiagRenderer {
pub fn new() -> Self {
let options = X86DiagnosticOptions::default();
let color_scheme = options.color_scheme();
let filter = X86DiagnosticFilter::default();
Self {
color_scheme: color_scheme.clone(),
term_renderer: X86TermRenderer::new(color_scheme.clone(), options.clone()),
json_renderer: X86JsonRenderer::new(),
sarif_renderer: X86SarifRenderer::new(),
xml_renderer: X86XmlRenderer::new(),
gcc_renderer: X86GccRenderer::new(),
msvc_renderer: X86MsvcRenderer::new(),
vi_renderer: X86ViRenderer::new(),
fixit_renderer: X86FixItRenderer::new(options.fixit_colors, options.tab_stop),
grouper: X86DiagGrouper::new(),
macro_backtrace: X86MacroBacktrace::new(
options.macro_backtrace_limit,
color_scheme.clone(),
),
include_stack: X86IncludeStack::new(options.include_stack_limit, color_scheme.clone()),
compactor: X86DiagnosticCompactor::new(),
line_cache: SourceLineCache::new(options.tab_stop),
error_count: 0,
warning_count: 0,
system_headers: HashSet::new(),
source_manager: None,
options,
filter,
}
}
pub fn with_options(options: X86DiagnosticOptions) -> Self {
let color_scheme = options.color_scheme();
let filter = X86DiagnosticFilter::default();
Self {
color_scheme: color_scheme.clone(),
term_renderer: X86TermRenderer::new(color_scheme.clone(), options.clone()),
json_renderer: X86JsonRenderer::new(),
sarif_renderer: X86SarifRenderer::new(),
xml_renderer: X86XmlRenderer::new(),
gcc_renderer: X86GccRenderer::new(),
msvc_renderer: X86MsvcRenderer::new(),
vi_renderer: X86ViRenderer::new(),
fixit_renderer: X86FixItRenderer::new(options.fixit_colors, options.tab_stop),
grouper: X86DiagGrouper::new(),
macro_backtrace: X86MacroBacktrace::new(
options.macro_backtrace_limit,
color_scheme.clone(),
),
include_stack: X86IncludeStack::new(options.include_stack_limit, color_scheme),
compactor: X86DiagnosticCompactor::new(),
line_cache: SourceLineCache::new(options.tab_stop),
error_count: 0,
warning_count: 0,
system_headers: HashSet::new(),
source_manager: None,
options,
filter,
}
}
pub fn set_filter(&mut self, filter: X86DiagnosticFilter) {
self.filter = filter;
}
pub fn add_system_header(&mut self, path: PathBuf) {
self.system_headers.insert(path);
}
pub fn set_source_manager(&mut self, source_manager: ClangSourceManager) {
self.source_manager = Some(source_manager);
}
pub fn set_options(&mut self, options: X86DiagnosticOptions) {
self.color_scheme = options.color_scheme();
self.term_renderer = X86TermRenderer::new(self.color_scheme.clone(), options.clone());
self.fixit_renderer = X86FixItRenderer::new(options.fixit_colors, options.tab_stop);
self.macro_backtrace =
X86MacroBacktrace::new(options.macro_backtrace_limit, self.color_scheme.clone());
self.include_stack =
X86IncludeStack::new(options.include_stack_limit, self.color_scheme.clone());
self.line_cache = SourceLineCache::new(options.tab_stop);
self.options = options;
}
pub fn render_terminal(
&mut self,
diags: &[StructuredDiagnostic],
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let filtered: Vec<&StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.collect();
let mut error_count = 0u32;
let display_diags: Vec<&StructuredDiagnostic> = filtered
.into_iter()
.take_while(|d| {
if let Some(limit) = self.filter.error_limit {
if d.severity.is_error() {
error_count += 1;
if error_count > limit {
return false;
}
}
}
true
})
.collect();
let owned_diags: Vec<StructuredDiagnostic> =
display_diags.iter().map(|d| (*d).clone()).collect();
let groups = self.grouper.process(owned_diags);
for group in &groups {
if self.options.show_macro_backtrace {
let trace = self
.macro_backtrace
.build_trace(&group.primary, source_manager);
if !trace.is_empty() {
let bt = self.macro_backtrace.render(&trace, source_manager);
output.push_str(&bt);
}
}
let rendered = self.term_renderer.render(&group.primary, source_manager);
output.push_str(&rendered);
for note in &group.notes {
if note.severity == DiagSeverity::Note {
let note_text = self.term_renderer.render(note, source_manager);
output.push_str(¬e_text);
}
}
if group.suppressed_count > 0 {
writeln!(
output,
"{}: {}",
self.color_scheme.note(&format!(
"note: {} additional occurrence(s) suppressed",
group.suppressed_count
)),
""
)
.unwrap();
}
output.push('\n');
}
let error_total: u32 = diags.iter().filter(|d| d.severity.is_error()).count() as u32;
let warn_total: u32 = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.count() as u32;
if error_total > 0 || warn_total > 0 {
writeln!(
output,
"{} error(s) generated, {} warning(s) generated.",
error_total, warn_total
)
.unwrap();
}
self.error_count = error_total;
self.warning_count = warn_total;
output
}
pub fn render_json(&self, diags: &[StructuredDiagnostic]) -> String {
let filtered: Vec<StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.cloned()
.collect();
self.json_renderer.render_diagnostics(&filtered)
}
pub fn render_sarif(&self, diags: &[StructuredDiagnostic]) -> String {
let filtered: Vec<StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.cloned()
.collect();
let source_files: Vec<String> = filtered
.iter()
.map(|d| d.location.file.to_string_lossy().to_string())
.collect::<HashSet<_>>()
.into_iter()
.collect();
let file_refs: Vec<&str> = source_files.iter().map(|s| s.as_str()).collect();
self.sarif_renderer.render_log(&filtered, &file_refs)
}
pub fn render_xml(&self, diags: &[StructuredDiagnostic]) -> String {
let filtered: Vec<StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.cloned()
.collect();
self.xml_renderer.render(&filtered)
}
pub fn render_gcc(&self, diags: &[StructuredDiagnostic]) -> String {
let filtered: Vec<StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.cloned()
.collect();
self.gcc_renderer.render_all(&filtered)
}
pub fn render_msvc(&self, diags: &[StructuredDiagnostic]) -> String {
let filtered: Vec<StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.cloned()
.collect();
self.msvc_renderer.render_all(&filtered)
}
pub fn render_vi(&self, diags: &[StructuredDiagnostic]) -> String {
let filtered: Vec<StructuredDiagnostic> = diags
.iter()
.filter(|d| self.filter.should_show(d, &self.system_headers))
.cloned()
.collect();
self.vi_renderer.render_all(&filtered)
}
pub fn render_fixit(&self, fixit: &FixItHint, source_manager: &ClangSourceManager) -> String {
self.fixit_renderer
.render_fixits(&[fixit.clone()], source_manager)
}
pub fn apply_fixits(
&self,
fixits: &[FixItHint],
source_manager: &ClangSourceManager,
) -> Result<HashMap<String, String>, String> {
self.fixit_renderer.apply_fixits(fixits, source_manager)
}
pub fn detect_fixit_conflicts(&self, fixits: &[FixItHint]) -> Vec<String> {
self.fixit_renderer.detect_conflicts(fixits)
}
pub fn render_macro_backtrace(
&self,
steps: &[MacroExpansionStep],
source_manager: &ClangSourceManager,
) -> String {
self.macro_backtrace.render(steps, source_manager)
}
pub fn render_include_stack(&self, stack: &[IncludeStackEntry]) -> String {
self.include_stack.render(stack)
}
pub fn compact_diagnostic(&self, diag: &StructuredDiagnostic) -> String {
self.compactor.compact(diag)
}
pub fn error_count(&self) -> u32 {
self.error_count
}
pub fn warning_count(&self) -> u32 {
self.warning_count
}
pub fn reset_counts(&mut self) {
self.error_count = 0;
self.warning_count = 0;
}
pub fn clear_cache(&mut self) {
self.line_cache.clear();
}
}
impl Default for X86DiagRenderer {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86DiagRenderer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("X86DiagRenderer")
.field("options", &self.options)
.field("error_count", &self.error_count)
.field("warning_count", &self.warning_count)
.finish()
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagnosticStats {
pub total: u32,
pub errors: u32,
pub warnings: u32,
pub notes: u32,
pub remarks: u32,
pub fatals: u32,
pub by_category: HashMap<String, u32>,
pub by_file: HashMap<String, u32>,
pub by_flag: HashMap<String, u32>,
}
impl X86DiagnosticStats {
pub fn new() -> Self {
Self::default()
}
pub fn collect(&mut self, diags: &[StructuredDiagnostic]) {
for diag in diags {
self.total += 1;
match diag.severity {
DiagSeverity::Error => self.errors += 1,
DiagSeverity::Warning => self.warnings += 1,
DiagSeverity::Note => self.notes += 1,
DiagSeverity::Remark => self.remarks += 1,
DiagSeverity::Fatal => self.fatals += 1,
DiagSeverity::Ignored => {}
}
*self.by_category.entry(diag.category.clone()).or_insert(0) += 1;
*self
.by_file
.entry(diag.location.file.to_string_lossy().to_string())
.or_insert(0) += 1;
if let Some(ref flag) = diag.flag_name {
*self.by_flag.entry(flag.clone()).or_insert(0) += 1;
}
}
}
pub fn format_summary(&self) -> String {
let mut output = String::new();
writeln!(output, "Diagnostic Statistics:").unwrap();
writeln!(output, " Total: {}", self.total).unwrap();
writeln!(output, " Errors: {}", self.errors).unwrap();
writeln!(output, " Warnings: {}", self.warnings).unwrap();
writeln!(output, " Notes: {}", self.notes).unwrap();
writeln!(output, " Remarks: {}", self.remarks).unwrap();
writeln!(output, " Fatals: {}", self.fatals).unwrap();
if !self.by_category.is_empty() {
writeln!(output, "\n By Category:").unwrap();
let mut cats: Vec<_> = self.by_category.iter().collect();
cats.sort_by(|a, b| b.1.cmp(a.1));
for (cat, count) in cats.iter().take(10) {
writeln!(output, " {}: {}", cat, count).unwrap();
}
}
if !self.by_file.is_empty() {
writeln!(output, "\n By File:").unwrap();
let mut files: Vec<_> = self.by_file.iter().collect();
files.sort_by(|a, b| b.1.cmp(a.1));
for (file, count) in files.iter().take(10) {
writeln!(output, " {}: {}", file, count).unwrap();
}
}
if !self.by_flag.is_empty() {
writeln!(output, "\n By Flag:").unwrap();
let mut flags: Vec<_> = self.by_flag.iter().collect();
flags.sort_by(|a, b| b.1.cmp(a.1));
for (flag, count) in flags.iter().take(10) {
writeln!(output, " {}: {}", flag, count).unwrap();
}
}
output
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
pub struct X86DiagnosticPipeline {
pub renderer: X86DiagRenderer,
pub stats: X86DiagnosticStats,
}
impl X86DiagnosticPipeline {
pub fn new() -> Self {
Self {
renderer: X86DiagRenderer::new(),
stats: X86DiagnosticStats::new(),
}
}
pub fn with_options(options: X86DiagnosticOptions) -> Self {
Self {
renderer: X86DiagRenderer::with_options(options),
stats: X86DiagnosticStats::new(),
}
}
pub fn process(
&mut self,
diags: &[StructuredDiagnostic],
format: X86DiagOutputFormat,
source_manager: Option<&ClangSourceManager>,
) -> X86DiagPipelineResult {
self.stats.collect(diags);
let output = match format {
X86DiagOutputFormat::Terminal => {
if let Some(sm) = source_manager {
self.renderer.render_terminal(diags, sm)
} else {
"Error: source_manager required for terminal output".to_string()
}
}
X86DiagOutputFormat::Json => self.renderer.render_json(diags),
X86DiagOutputFormat::Sarif => self.renderer.render_sarif(diags),
X86DiagOutputFormat::Xml => self.renderer.render_xml(diags),
X86DiagOutputFormat::Gcc => self.renderer.render_gcc(diags),
X86DiagOutputFormat::Msvc => self.renderer.render_msvc(diags),
X86DiagOutputFormat::Vi => self.renderer.render_vi(diags),
X86DiagOutputFormat::Compact => diags
.iter()
.map(|d| self.renderer.compact_diagnostic(d))
.collect::<Vec<_>>()
.join("\n"),
X86DiagOutputFormat::Stats => self.stats.format_summary(),
};
X86DiagPipelineResult {
output,
error_count: self.renderer.error_count(),
warning_count: self.renderer.warning_count(),
stats: self.stats.clone(),
}
}
pub fn renderer(&self) -> &X86DiagRenderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut X86DiagRenderer {
&mut self.renderer
}
}
impl Default for X86DiagnosticPipeline {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86DiagOutputFormat {
Terminal,
Json,
Sarif,
Xml,
Gcc,
Msvc,
Vi,
Compact,
Stats,
}
#[derive(Debug, Clone)]
pub struct X86DiagPipelineResult {
pub output: String,
pub error_count: u32,
pub warning_count: u32,
pub stats: X86DiagnosticStats,
}
#[derive(Debug, Clone)]
pub struct TemplateInstantiationNote {
pub template_name: String,
pub definition_loc: ClangSourceLocation,
pub instantiation_loc: ClangSourceLocation,
pub depth: u32,
}
#[derive(Debug, Clone)]
pub struct X86TemplateBacktrace {
pub max_depth: u32,
}
impl X86TemplateBacktrace {
pub fn new(max_depth: u32) -> Self {
Self { max_depth }
}
pub fn render(&self, notes: &[TemplateInstantiationNote]) -> String {
if notes.is_empty() {
return String::new();
}
let mut output = String::new();
for note in notes.iter().take(self.max_depth as usize) {
let indent = " ".repeat(note.depth as usize);
if note.depth == 0 {
output.push_str(&format!(
"{}in instantiation of template '{}' requested here\n",
indent, note.template_name
));
} else {
output.push_str(&format!(
"{}in instantiation of template '{}'\n",
indent, note.template_name
));
}
if note.instantiation_loc.is_valid {
output.push_str(&format!(
"{} at {}:{}:{}\n",
indent,
note.instantiation_loc.file.display(),
note.instantiation_loc.line,
note.instantiation_loc.column
));
}
if note.definition_loc.is_valid {
output.push_str(&format!(
"{} template defined at {}:{}:{}\n",
indent,
note.definition_loc.file.display(),
note.definition_loc.line,
note.definition_loc.column
));
}
}
output
}
}
impl Default for X86TemplateBacktrace {
fn default() -> Self {
Self::new(10)
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagnosticSummary;
impl X86DiagnosticSummary {
pub fn new() -> Self {
Self
}
pub fn render_summary(
&self,
diags: &[StructuredDiagnostic],
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
let errors: Vec<_> = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Error || d.severity == DiagSeverity::Fatal)
.collect();
let warnings: Vec<_> = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.collect();
let notes: Vec<_> = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Note)
.collect();
let fixit_count: usize = diags.iter().map(|d| d.fixits.len()).sum();
writeln!(output, "=== Diagnostic Summary ===").unwrap();
writeln!(output, "Errors: {}", errors.len()).unwrap();
writeln!(output, "Warnings: {}", warnings.len()).unwrap();
writeln!(output, "Notes: {}", notes.len()).unwrap();
writeln!(output, "Fix-Its: {}", fixit_count).unwrap();
if fixit_count > 0 {
writeln!(output, "\n--- Fix-It Hints ---").unwrap();
for diag in diags {
if !diag.fixits.is_empty() {
writeln!(
output,
" {}:{}:{} - {} ({})",
&diag
.location
.file
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| diag.location.file.to_string_lossy().to_string()),
diag.location.line,
diag.location.column,
&diag.message[..diag.message.len().min(60)],
diag.fixits.len()
)
.unwrap();
for fixit in &diag.fixits {
if fixit.is_insertion {
writeln!(output, " INSERT: \"{}\"", fixit.code).unwrap();
} else if fixit.code.is_empty() {
writeln!(
output,
" REMOVE: {}:{}-{}:{}",
fixit.remove_range.begin.line,
fixit.remove_range.begin.column,
fixit.remove_range.end.line,
fixit.remove_range.end.column
)
.unwrap();
} else {
writeln!(output, " REPLACE: \"{}\"", fixit.code).unwrap();
}
}
}
}
}
output
}
pub fn has_errors(diags: &[StructuredDiagnostic]) -> bool {
diags
.iter()
.any(|d| d.severity == DiagSeverity::Error || d.severity == DiagSeverity::Fatal)
}
pub fn has_fixits(diags: &[StructuredDiagnostic]) -> bool {
diags.iter().any(|d| !d.fixits.is_empty())
}
pub fn count_by_category(diags: &[StructuredDiagnostic]) -> HashMap<String, u32> {
let mut counts = HashMap::new();
for diag in diags {
*counts.entry(diag.category.clone()).or_insert(0) += 1;
}
counts
}
}
#[derive(Debug, Clone)]
pub struct X86DiagnosticFileWriter {
pub output_path: Option<PathBuf>,
pub append: bool,
}
impl X86DiagnosticFileWriter {
pub fn to_stdout() -> Self {
Self {
output_path: None,
append: false,
}
}
pub fn to_file(path: PathBuf) -> Self {
Self {
output_path: Some(path),
append: false,
}
}
pub fn append(mut self, append: bool) -> Self {
self.append = append;
self
}
pub fn write(&self, content: &str) -> io::Result<()> {
if let Some(ref path) = self.output_path {
if self.append {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?
.write_all(content.as_bytes())?;
} else {
std::fs::write(path, content)?;
}
} else {
let stdout = io::stdout();
let mut handle = stdout.lock();
handle.write_all(content.as_bytes())?;
handle.flush()?;
}
Ok(())
}
}
pub fn make_terminal_renderer(is_tty: bool) -> X86DiagRenderer {
let mut options = X86DiagnosticOptions::for_terminal();
options.is_tty = is_tty;
X86DiagRenderer::with_options(options)
}
pub fn make_ide_renderer() -> X86DiagRenderer {
X86DiagRenderer::with_options(X86DiagnosticOptions::for_ide())
}
pub fn make_machine_renderer() -> X86DiagRenderer {
X86DiagRenderer::with_options(X86DiagnosticOptions::for_machine())
}
pub fn render_diag_terminal(
diag: &StructuredDiagnostic,
source_manager: &ClangSourceManager,
) -> String {
let renderer = X86DiagRenderer::new();
let mut term = X86TermRenderer::new(
X86ColorScheme::clang_style(false),
X86DiagnosticOptions::for_terminal(),
);
term.render(diag, source_manager)
}
pub fn apply_fixit_hints(
fixits: &[FixItHint],
source_manager: &ClangSourceManager,
) -> Result<HashMap<String, String>, String> {
let fixit_renderer = X86FixItRenderer::default();
fixit_renderer.apply_fixits(fixits, source_manager)
}
pub fn detect_fixit_conflicts(fixits: &[FixItHint]) -> Vec<String> {
let fixit_renderer = X86FixItRenderer::default();
fixit_renderer.detect_conflicts(fixits)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clang::diagnostics::{
ClangSourceFile, ClangSourceLocation, ClangSourceManager, DiagID, DiagSeverity,
DiagnosticNote, FixItHint, SourceRange, StructuredDiagnostic,
};
fn make_test_source_manager() -> ClangSourceManager {
let mut sm = ClangSourceManager::new();
let content = "\
int main() {\n\
int x = 0;\n\
int y = x / 0;\n\
return y;\n\
}\n";
let src_file = ClangSourceFile::new(Path::new("test.c"), &content);
sm.add_file(Path::new("test.c"), &src_file.content);
sm
}
fn make_test_diag() -> StructuredDiagnostic {
StructuredDiagnostic {
id: DiagID::ErrDivisionByZeroConst,
severity: DiagSeverity::Error,
message: "division by zero is undefined".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 17,
offset: 35 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 17,
offset: 35 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 22,
offset: 40 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
}],
notes: vec![],
fixits: vec![],
flag_name: Some("-Wdivision-by-zero".to_string()),
category: "Semantic Issue".to_string(),
}
}
fn make_test_warning_diag() -> StructuredDiagnostic {
StructuredDiagnostic {
id: DiagID::WUnusedVariable,
severity: DiagSeverity::Warning,
message: "unused variable 'x'".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 2,
column: 9,
offset: 20 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
notes: vec![],
fixits: vec![],
flag_name: Some("-Wunused-variable".to_string()),
category: "Semantic Issue".to_string(),
}
}
fn make_test_diag_with_note() -> StructuredDiagnostic {
let mut diag = make_test_diag();
diag.notes.push(DiagnosticNote {
message: "variable 'y' declared here".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 2,
column: 9,
offset: 20 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
});
diag
}
fn make_test_diag_with_fixit() -> StructuredDiagnostic {
let mut diag = make_test_diag();
diag.fixits.push(FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 13,
offset: 31 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 23,
offset: 41 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1 /* not zero */".to_string(),
is_insertion: false,
});
diag
}
#[test]
fn test_unicode_column_width_ascii() {
assert_eq!(unicode_column_width("hello", 8), 5);
assert_eq!(unicode_column_width("abc123", 8), 6);
}
#[test]
fn test_unicode_column_width_tabs() {
assert_eq!(unicode_column_width("\thello", 4), 8);
assert_eq!(unicode_column_width("a\tb", 4), 5);
assert_eq!(unicode_column_width("ab\tc", 4), 6);
}
#[test]
fn test_unicode_column_width_cjk() {
let width = unicode_column_width("你好", 8);
assert_eq!(width, 4);
}
#[test]
fn test_expand_tabs() {
assert_eq!(expand_tabs("a\tb", 4), "a b");
assert_eq!(expand_tabs("\tleading", 4), " leading");
assert_eq!(expand_tabs("no_tabs", 4), "no_tabs");
}
#[test]
fn test_column_to_byte_offset() {
assert_eq!(column_to_byte_offset("hello", 3, 8), 2);
assert_eq!(column_to_byte_offset("hello", 1, 8), 0);
}
#[test]
fn test_color_scheme_clang_style() {
let scheme = X86ColorScheme::clang_style(true);
let err = scheme.error("error message");
assert!(err.contains(ansi::BRIGHT_RED));
assert!(err.contains(ansi::RESET));
assert!(err.contains("error message"));
}
#[test]
fn test_color_scheme_monochrome() {
let scheme = X86ColorScheme::monochrome();
let err = scheme.error("error message");
assert_eq!(err, "error message");
}
#[test]
fn test_color_scheme_disabled() {
let scheme = X86ColorScheme::clang_style(false);
let err = scheme.error("error message");
assert_eq!(err, "error message");
}
#[test]
fn test_options_default() {
let opts = X86DiagnosticOptions::default();
assert!(opts.show_carets);
assert!(opts.show_column_numbers);
assert_eq!(opts.tab_stop, 8);
assert_eq!(opts.message_length_limit, 0);
}
#[test]
fn test_options_for_terminal() {
let opts = X86DiagnosticOptions::for_terminal();
assert!(opts.show_carets);
assert!(opts.show_fixits);
assert_eq!(opts.macro_backtrace_limit, 10);
}
#[test]
fn test_options_for_ide() {
let opts = X86DiagnosticOptions::for_ide();
assert!(!opts.show_carets);
assert!(!opts.show_fixits);
assert_eq!(opts.color_mode, ColorMode::Never);
}
#[test]
fn test_options_use_colors() {
let mut opts = X86DiagnosticOptions::for_terminal();
opts.color_mode = ColorMode::Always;
assert!(opts.use_colors());
opts.color_mode = ColorMode::Never;
assert!(!opts.use_colors());
opts.color_mode = ColorMode::Auto;
opts.is_tty = true;
assert!(opts.use_colors());
opts.is_tty = false;
assert!(!opts.use_colors());
}
#[test]
fn test_filter_no_filter() {
let filter = X86DiagnosticFilter::new();
let diag = make_test_diag();
let sys_headers = HashSet::new();
assert!(filter.should_show(&diag, &sys_headers));
}
#[test]
fn test_filter_errors_only() {
let filter = X86DiagnosticFilter::errors_only();
let sys_headers = HashSet::new();
let err_diag = make_test_diag();
assert!(filter.should_show(&err_diag, &sys_headers));
let warn_diag = make_test_warning_diag();
assert!(!filter.should_show(&warn_diag, &sys_headers));
}
#[test]
fn test_filter_disable_warning() {
let filter = X86DiagnosticFilter::new().disable_warning("-Wunused-variable");
let sys_headers = HashSet::new();
let warn_diag = make_test_warning_diag();
assert!(!filter.should_show(&warn_diag, &sys_headers));
}
#[test]
fn test_filter_line_range() {
let filter = X86DiagnosticFilter::new().line_range(1, 2);
let sys_headers = HashSet::new();
let warn_diag = make_test_warning_diag(); assert!(filter.should_show(&warn_diag, &sys_headers));
let err_diag = make_test_diag(); assert!(!filter.should_show(&err_diag, &sys_headers));
}
#[test]
fn test_filter_only_with_fixits() {
let filter = X86DiagnosticFilter::new().only_with_fixits();
let sys_headers = HashSet::new();
let diag_no_fixit = make_test_diag();
assert!(!filter.should_show(&diag_no_fixit, &sys_headers));
let diag_with_fixit = make_test_diag_with_fixit();
assert!(filter.should_show(&diag_with_fixit, &sys_headers));
}
#[test]
fn test_filter_by_category() {
let filter = X86DiagnosticFilter::new().include_category("Lexical Issue");
let sys_headers = HashSet::new();
let diag = make_test_diag(); assert!(!filter.should_show(&diag, &sys_headers));
}
#[test]
fn test_term_renderer_basic() {
let scheme = X86ColorScheme::monochrome();
let opts = X86DiagnosticOptions::for_terminal();
let renderer = X86TermRenderer::new(scheme, opts);
let sm = make_test_source_manager();
let diag = make_test_diag();
let output = renderer.render(&diag, &sm);
assert!(output.contains("error:"));
assert!(output.contains("division by zero"));
assert!(output.contains("test.c"));
}
#[test]
fn test_term_renderer_with_carets() {
let scheme = X86ColorScheme::monochrome();
let mut opts = X86DiagnosticOptions::for_terminal();
opts.show_carets = true;
let renderer = X86TermRenderer::new(scheme, opts);
let sm = make_test_source_manager();
let diag = make_test_diag();
let output = renderer.render(&diag, &sm);
assert!(output.contains("|"));
assert!(output.contains("^"));
}
#[test]
fn test_term_renderer_with_fixit() {
let scheme = X86ColorScheme::monochrome();
let mut opts = X86DiagnosticOptions::for_terminal();
opts.show_fixits = true;
let renderer = X86TermRenderer::new(scheme, opts);
let sm = make_test_source_manager();
let diag = make_test_diag_with_fixit();
let output = renderer.render(&diag, &sm);
assert!(output.contains("fix-it"));
}
#[test]
fn test_term_renderer_with_note() {
let scheme = X86ColorScheme::monochrome();
let opts = X86DiagnosticOptions::for_terminal();
let renderer = X86TermRenderer::new(scheme, opts);
let sm = make_test_source_manager();
let diag = make_test_diag_with_note();
let output = renderer.render(&diag, &sm);
assert!(output.contains("note:"));
}
#[test]
fn test_json_renderer_single() {
let renderer = X86JsonRenderer::new();
let diag = make_test_diag();
let output = renderer.render_single(&diag);
assert!(output.contains("\"severity\""));
assert!(output.contains("error"));
assert!(output.contains("division by zero"));
}
#[test]
fn test_json_renderer_multiple() {
let renderer = X86JsonRenderer::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = renderer.render_diagnostics(&diags);
assert!(output.starts_with("[\n"));
assert!(output.contains("error"));
assert!(output.contains("warning"));
}
#[test]
fn test_json_escape() {
assert_eq!(json_escape("hello"), "\"hello\"");
assert_eq!(json_escape("line\nbreak"), "\"line\\nbreak\"");
assert_eq!(json_escape("quote\"here"), "\"quote\\\"here\"");
assert_eq!(json_escape("tab\there"), "\"tab\\there\"");
}
#[test]
fn test_sarif_renderer_basic() {
let renderer = X86SarifRenderer::new();
let diags = vec![make_test_diag()];
let files = vec!["test.c"];
let output = renderer.render_log(&diags, &files);
assert!(output.contains("\"$schema\""));
assert!(output.contains("sarif-schema-2.1.0"));
assert!(output.contains("\"version\": \"2.1.0\""));
assert!(output.contains("\"runs\""));
assert!(output.contains("\"results\""));
}
#[test]
fn test_sarif_renderer_with_tool_info() {
let renderer =
X86SarifRenderer::with_tool_info("custom-clang", "1.0.0", "https://example.com/");
let diags = vec![make_test_diag()];
let files = vec!["test.c"];
let output = renderer.render_log(&diags, &files);
assert!(output.contains("custom-clang"));
assert!(output.contains("1.0.0"));
assert!(output.contains("https://example.com/"));
}
#[test]
fn test_xml_renderer_basic() {
let renderer = X86XmlRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(output.starts_with("<?xml"));
assert!(output.contains("<checkstyle"));
assert!(output.contains("<file name=\"test.c\""));
assert!(output.contains("error"));
}
#[test]
fn test_xml_escape() {
assert_eq!(xml_escape("a < b"), "a < b");
assert_eq!(xml_escape("a > b"), "a > b");
assert_eq!(xml_escape("a & b"), "a & b");
assert_eq!(xml_escape("\"quoted\""), ""quoted"");
}
#[test]
fn test_gcc_renderer_basic() {
let renderer = X86GccRenderer::new();
let diag = make_test_diag();
let output = renderer.render(&diag);
assert!(output.contains("test.c:3:17:"));
assert!(output.contains("error:"));
assert!(output.contains("[-Wdivision-by-zero]"));
}
#[test]
fn test_gcc_renderer_warning() {
let renderer = X86GccRenderer::new();
let diag = make_test_warning_diag();
let output = renderer.render(&diag);
assert!(output.contains("warning:"));
assert!(output.contains("[-Wunused-variable]"));
}
#[test]
fn test_gcc_renderer_all() {
let renderer = X86GccRenderer::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = renderer.render_all(&diags);
assert_eq!(output.lines().count(), 2);
}
#[test]
fn test_msvc_renderer_basic() {
let renderer = X86MsvcRenderer::new();
let diag = make_test_diag();
let output = renderer.render(&diag);
assert!(output.contains("test.c(3,17)"));
assert!(output.contains("error C"));
assert!(output.contains("division by zero"));
}
#[test]
fn test_msvc_renderer_all() {
let renderer = X86MsvcRenderer::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = renderer.render_all(&diags);
assert_eq!(output.lines().count(), 2);
}
#[test]
fn test_vi_renderer_basic() {
let renderer = X86ViRenderer::new();
let diag = make_test_diag();
let output = renderer.render(&diag);
assert!(output.starts_with("test.c +3"));
}
#[test]
fn test_vi_renderer_all() {
let renderer = X86ViRenderer::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = renderer.render_all(&diags);
assert_eq!(output.lines().count(), 2);
}
#[test]
fn test_fixit_renderer_detect_no_conflicts() {
let renderer = X86FixItRenderer::default();
let fixit1 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 13,
offset: 31 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 23,
offset: 41 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let fixit2 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 5,
column: 1,
offset: 50 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 5,
column: 5,
offset: 54 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "".to_string(),
is_insertion: false,
};
let conflicts = renderer.detect_conflicts(&[fixit1, fixit2]);
assert!(conflicts.is_empty());
}
#[test]
fn test_fixit_renderer_detect_conflicts() {
let renderer = X86FixItRenderer::default();
let fixit1 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 13,
offset: 31 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 20,
offset: 38 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let fixit2 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 17,
offset: 35 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 25,
offset: 43 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "2".to_string(),
is_insertion: false,
};
let conflicts = renderer.detect_conflicts(&[fixit1, fixit2]);
assert!(!conflicts.is_empty());
assert!(conflicts[0].contains("overlap"));
}
#[test]
fn test_grouper_deduplication() {
let mut grouper = X86DiagGrouper::new().with_dedup_limit(2);
let diags = vec![make_test_diag(), make_test_diag(), make_test_diag()];
let groups = grouper.process(diags);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].occurrence_count, 1);
}
#[test]
fn test_grouper_no_dedup_without_limit() {
let mut grouper = X86DiagGrouper::new().with_dedup_limit(5);
let diags = vec![make_test_diag(), make_test_diag()];
let groups = grouper.process(diags);
assert!(!groups.is_empty());
}
#[test]
fn test_macro_backtrace_empty() {
let scheme = X86ColorScheme::monochrome();
let bt = X86MacroBacktrace::new(10, scheme);
let steps: Vec<MacroExpansionStep> = vec![];
let sm = make_test_source_manager();
let output = bt.render(&steps, &sm);
assert!(output.is_empty());
}
#[test]
fn test_macro_backtrace_single() {
let scheme = X86ColorScheme::monochrome();
let bt = X86MacroBacktrace::new(10, scheme);
let sm = make_test_source_manager();
let steps = vec![MacroExpansionStep {
macro_name: "MY_MACRO".to_string(),
definition_location: ClangSourceLocation {
file: PathBuf::from("header.h"),
line: 10,
column: 9,
offset: 100 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
expansion_location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 1,
offset: 30 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
depth: 0,
source_context: Some("MY_MACRO(x)".to_string()),
}];
let output = bt.render(&steps, &sm);
assert!(output.contains("MY_MACRO"));
assert!(output.contains("expansion"));
}
#[test]
fn test_include_stack_empty() {
let scheme = X86ColorScheme::monochrome();
let stack = X86IncludeStack::new(10, scheme);
let entries: Vec<IncludeStackEntry> = vec![];
let output = stack.render(&entries);
assert!(output.is_empty());
}
#[test]
fn test_include_stack_single() {
let scheme = X86ColorScheme::monochrome();
let stack = X86IncludeStack::new(10, scheme);
let entries = vec![IncludeStackEntry {
file: "test.c".to_string(),
include_line: 1,
including_file: "main.c".to_string(),
depth: 0,
}];
let output = stack.render(&entries);
assert!(output.contains("test.c"));
assert!(output.contains("main.c"));
}
#[test]
fn test_diag_renderer_terminal() {
let mut renderer = X86DiagRenderer::new();
let sm = make_test_source_manager();
let diags = vec![make_test_diag()];
let output = renderer.render_terminal(&diags, &sm);
assert!(output.contains("error"));
assert!(output.contains("division by zero"));
}
#[test]
fn test_diag_renderer_json() {
let renderer = X86DiagRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render_json(&diags);
assert!(output.contains("["));
assert!(output.contains("\"severity\""));
}
#[test]
fn test_diag_renderer_sarif() {
let renderer = X86DiagRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render_sarif(&diags);
assert!(output.contains("sarif"));
}
#[test]
fn test_diag_renderer_gcc() {
let renderer = X86DiagRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render_gcc(&diags);
assert!(output.contains("test.c:3:17:"));
}
#[test]
fn test_diag_renderer_msvc() {
let renderer = X86DiagRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render_msvc(&diags);
assert!(output.contains("test.c(3,17)"));
}
#[test]
fn test_diag_renderer_vi() {
let renderer = X86DiagRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render_vi(&diags);
assert!(output.contains("test.c +3"));
}
#[test]
fn test_diag_renderer_xml() {
let renderer = X86DiagRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render_xml(&diags);
assert!(output.contains("<?xml"));
}
#[test]
fn test_diag_renderer_counts() {
let mut renderer = X86DiagRenderer::new();
let sm = make_test_source_manager();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let _output = renderer.render_terminal(&diags, &sm);
assert_eq!(renderer.error_count(), 1);
assert_eq!(renderer.warning_count(), 1);
renderer.reset_counts();
assert_eq!(renderer.error_count(), 0);
}
#[test]
fn test_diag_renderer_filter_integration() {
let mut renderer = X86DiagRenderer::new();
renderer.set_filter(X86DiagnosticFilter::errors_only());
let sm = make_test_source_manager();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = renderer.render_terminal(&diags, &sm);
assert!(output.contains("error"));
}
#[test]
fn test_compactor_basic() {
let compactor = X86DiagnosticCompactor::new();
let diag = make_test_diag();
let output = compactor.compact(&diag);
assert!(output.contains("[ERROR]"));
assert!(output.contains("division by zero"));
}
#[test]
fn test_stats_collect() {
let mut stats = X86DiagnosticStats::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
stats.collect(&diags);
assert_eq!(stats.total, 2);
assert_eq!(stats.errors, 1);
assert_eq!(stats.warnings, 1);
}
#[test]
fn test_stats_summary() {
let mut stats = X86DiagnosticStats::new();
let diags = vec![make_test_diag()];
stats.collect(&diags);
let summary = stats.format_summary();
assert!(summary.contains("Total"));
assert!(summary.contains("Errors"));
}
#[test]
fn test_stats_reset() {
let mut stats = X86DiagnosticStats::new();
let diags = vec![make_test_diag()];
stats.collect(&diags);
assert_eq!(stats.total, 1);
stats.reset();
assert_eq!(stats.total, 0);
}
#[test]
fn test_pipeline_terminal() {
let mut pipeline = X86DiagnosticPipeline::new();
let diags = vec![make_test_diag()];
let sm = make_test_source_manager();
let result = pipeline.process(&diags, X86DiagOutputFormat::Terminal, Some(&sm));
assert!(result.output.contains("error"));
assert_eq!(result.error_count, 1);
}
#[test]
fn test_pipeline_json() {
let mut pipeline = X86DiagnosticPipeline::new();
let diags = vec![make_test_diag()];
let result = pipeline.process(&diags, X86DiagOutputFormat::Json, None);
assert!(result.output.contains("\"severity\""));
}
#[test]
fn test_pipeline_stats() {
let mut pipeline = X86DiagnosticPipeline::new();
let diags = vec![make_test_diag()];
let result = pipeline.process(&diags, X86DiagOutputFormat::Stats, None);
assert!(result.output.contains("Diagnostic Statistics"));
}
#[test]
fn test_template_backtrace_basic() {
let bt = X86TemplateBacktrace::new(10);
let notes = vec![TemplateInstantiationNote {
template_name: "MyTemplate".to_string(),
definition_loc: ClangSourceLocation {
file: PathBuf::from("header.h"),
line: 5,
column: 1,
offset: 50 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
instantiation_loc: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 10,
column: 1,
offset: 150 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
depth: 0,
}];
let output = bt.render(¬es);
assert!(output.contains("MyTemplate"));
assert!(output.contains("instantiation"));
}
#[test]
fn test_template_backtrace_empty() {
let bt = X86TemplateBacktrace::new(10);
let notes: Vec<TemplateInstantiationNote> = vec![];
let output = bt.render(¬es);
assert!(output.is_empty());
}
#[test]
fn test_summary_basic() {
let summary = X86DiagnosticSummary::new();
let sm = make_test_source_manager();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = summary.render_summary(&diags, &sm);
assert!(output.contains("Errors:"));
assert!(output.contains("Warnings:"));
}
#[test]
fn test_summary_has_errors() {
let diags = vec![make_test_diag()];
assert!(X86DiagnosticSummary::has_errors(&diags));
let warn_diags = vec![make_test_warning_diag()];
assert!(!X86DiagnosticSummary::has_errors(&warn_diags));
}
#[test]
fn test_summary_has_fixits() {
let diags_no_fixit = vec![make_test_diag()];
assert!(!X86DiagnosticSummary::has_fixits(&diags_no_fixit));
let diags_with_fixit = vec![make_test_diag_with_fixit()];
assert!(X86DiagnosticSummary::has_fixits(&diags_with_fixit));
}
#[test]
fn test_make_terminal_renderer() {
let renderer = make_terminal_renderer(true);
assert!(renderer.options.is_tty);
}
#[test]
fn test_make_ide_renderer() {
let renderer = make_ide_renderer();
assert!(!renderer.options.show_carets);
}
#[test]
fn test_make_machine_renderer() {
let renderer = make_machine_renderer();
assert!(!renderer.options.show_carets);
assert!(!renderer.options.show_fixits);
}
#[test]
fn test_filter_error_limit() {
let mut renderer = X86DiagRenderer::new();
let filter = X86DiagnosticFilter::new().error_limit(1);
renderer.set_filter(filter);
let sm = make_test_source_manager();
let mut diag2 = make_test_diag();
diag2.id = DiagID::ErrUndeclaredIdentifier;
let diags = vec![make_test_diag(), diag2];
let output = renderer.render_terminal(&diags, &sm);
assert!(output.contains("division by zero"));
}
#[test]
fn test_identical_diag_detection() {
let diag1 = make_test_diag();
let diag2 = make_test_diag();
let fp1 = X86DiagGrouper::new().fingerprint(&diag1);
let fp2 = X86DiagGrouper::new().fingerprint(&diag2);
assert_eq!(fp1, fp2);
}
#[test]
fn test_source_context_lines() {
let mut sm = ClangSourceManager::new();
let content = "\
line 1\n\
line 2\n\
line 3 ERROR HERE\n\
line 4\n\
line 5\n";
let src_file = ClangSourceFile::new(Path::new("multi.c"), &content);
sm.add_file(Path::new("multi.c"), &src_file.content);
let scheme = X86ColorScheme::monochrome();
let mut opts = X86DiagnosticOptions::for_terminal();
opts.context_lines_before = 1;
opts.context_lines_after = 1;
let renderer = X86TermRenderer::new(scheme, opts);
let diag = StructuredDiagnostic {
id: DiagID::ErrUndeclaredIdentifier,
severity: DiagSeverity::Error,
message: "test error".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("multi.c"),
line: 3,
column: 7,
offset: 20 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
notes: vec![],
fixits: vec![],
flag_name: None,
category: "Test".to_string(),
};
let output = renderer.render(&diag, &sm);
assert!(output.contains("line 2"));
assert!(output.contains("line 4"));
}
#[test]
fn test_apply_fixits_no_conflict() {
let mut sm = ClangSourceManager::new();
let content = "int x = 0 / 0;\n";
let src_file = ClangSourceFile::new(Path::new("apply.c"), &content);
sm.add_file(Path::new("apply.c"), &src_file.content);
let fixit = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("apply.c"),
line: 1,
column: 11,
offset: 10 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("apply.c"),
line: 1,
column: 14,
offset: 13 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let renderer = X86FixItRenderer::default();
let result = renderer.apply_fixits(&[fixit], &sm);
assert!(result.is_ok());
let files = result.unwrap();
assert!(files.contains_key("apply.c"));
}
#[test]
fn test_apply_fixits_with_conflict() {
let mut sm = ClangSourceManager::new();
let content = "int x = 0 / 0;\n";
let src_file = ClangSourceFile::new(Path::new("conflict.c"), &content);
sm.add_file(Path::new("conflict.c"), &src_file.content);
let fixit1 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("conflict.c"),
line: 1,
column: 9,
offset: 8 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("conflict.c"),
line: 1,
column: 12,
offset: 11 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let fixit2 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("conflict.c"),
line: 1,
column: 10,
offset: 9 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("conflict.c"),
line: 1,
column: 13,
offset: 12 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "2".to_string(),
is_insertion: false,
};
let renderer = X86FixItRenderer::default();
let result = renderer.apply_fixits(&[fixit1, fixit2], &sm);
assert!(result.is_err());
}
#[test]
fn test_invalid_location_diag() {
let scheme = X86ColorScheme::monochrome();
let opts = X86DiagnosticOptions::for_terminal();
let renderer = X86TermRenderer::new(scheme, opts);
let sm = make_test_source_manager();
let diag = StructuredDiagnostic {
id: DiagID::FatalNoInputFiles,
severity: DiagSeverity::Fatal,
message: "no input files".to_string(),
location: ClangSourceLocation::invalid(),
ranges: vec![],
notes: vec![],
fixits: vec![],
flag_name: None,
category: "Driver".to_string(),
};
let output = renderer.render(&diag, &sm);
assert!(output.contains("no input files"));
assert!(!output.contains("test.c"));
}
#[test]
fn test_empty_diag_list() {
let mut renderer = X86DiagRenderer::new();
let sm = make_test_source_manager();
let output = renderer.render_terminal(&[], &sm);
assert!(output.contains("0 error"));
}
#[test]
fn test_severity_colors() {
let scheme = X86ColorScheme::clang_style(true);
let renderer = X86TermRenderer::new(scheme, X86DiagnosticOptions::for_terminal());
let sm = make_test_source_manager();
let err_diag = make_test_diag();
let output = renderer.render(&err_diag, &sm);
assert!(output.contains(ansi::BRIGHT_RED) || output.contains(ansi::RED));
let warn_diag = make_test_warning_diag();
let output = renderer.render(&warn_diag, &sm);
assert!(output.contains(ansi::BRIGHT_MAGENTA) || output.contains(ansi::MAGENTA));
}
#[test]
fn test_message_truncation() {
let mut opts = X86DiagnosticOptions::for_terminal();
opts.message_length_limit = 20;
let scheme = X86ColorScheme::monochrome();
let renderer = X86TermRenderer::new(scheme, opts);
let sm = make_test_source_manager();
let mut diag = make_test_diag();
diag.message = "this is a very long message that should be truncated".to_string();
let output = renderer.render(&diag, &sm);
assert!(output.contains(&diag.message));
}
#[test]
fn test_gcc_format_has_correct_structure() {
let renderer = X86GccRenderer::new();
let diag = make_test_diag();
let output = renderer.render(&diag);
let parts: Vec<&str> = output.splitn(5, ": ").collect();
assert!(parts.len() >= 2);
assert!(parts[0].contains("test.c"));
assert!(parts[0].contains(":3:17"));
}
#[test]
fn test_msvc_format_has_correct_structure() {
let renderer = X86MsvcRenderer::new();
let diag = make_test_diag();
let output = renderer.render(&diag);
assert!(output.contains("test.c(3,17)"));
assert!(output.contains("error"));
}
#[test]
fn test_sarif_output_is_valid_json() {
let renderer = X86SarifRenderer::new();
let diags = vec![make_test_diag()];
let files = vec!["test.c"];
let output = renderer.render_log(&diags, &files);
assert!(output.starts_with("{"));
assert!(output.ends_with("}\n"));
assert!(output.contains("\"$schema\""));
assert!(output.contains("\"version\": \"2.1.0\""));
assert!(output.contains("\"runs\""));
}
#[test]
fn test_unicode_combining_characters() {
assert_eq!(unicode_char_width('\u{0301}'), 0); assert_eq!(unicode_char_width('\u{0300}'), 0); assert_eq!(unicode_char_width('\u{0327}'), 0); }
#[test]
fn test_unicode_zero_width_characters() {
assert_eq!(unicode_char_width('\u{200B}'), 0); assert_eq!(unicode_char_width('\u{FEFF}'), 0); assert_eq!(unicode_char_width('\u{200D}'), 0); }
#[test]
fn test_unicode_fullwidth() {
assert_eq!(unicode_char_width('\u{FF21}'), 2); assert_eq!(unicode_char_width('\u{4E00}'), 2); }
#[test]
fn test_color_mode_auto() {
assert!(ColorMode::Auto.should_use_color(true));
assert!(!ColorMode::Auto.should_use_color(false));
}
#[test]
fn test_color_mode_always() {
assert!(ColorMode::Always.should_use_color(true));
assert!(ColorMode::Always.should_use_color(false));
}
#[test]
fn test_color_mode_never() {
assert!(!ColorMode::Never.should_use_color(true));
assert!(!ColorMode::Never.should_use_color(false));
}
#[test]
fn test_severity_filter_allows_higher() {
let filter = X86DiagnosticFilter::new().min_severity(DiagSeverity::Warning);
let sys_headers = HashSet::new();
let err_diag = make_test_diag(); assert!(filter.should_show(&err_diag, &sys_headers));
let warn_diag = make_test_warning_diag(); assert!(filter.should_show(&warn_diag, &sys_headers));
}
#[test]
fn test_severity_filter_blocks_lower() {
let filter = X86DiagnosticFilter::new().min_severity(DiagSeverity::Error);
let sys_headers = HashSet::new();
let warn_diag = make_test_warning_diag(); assert!(!filter.should_show(&warn_diag, &sys_headers));
}
#[test]
fn test_fixit_colored_diff() {
let renderer = X86TermRenderer::new(X86ColorScheme::clang_style(true), {
let mut opts = X86DiagnosticOptions::for_terminal();
opts.fixit_colors = true;
opts
});
let sm = make_test_source_manager();
let diag = make_test_diag_with_fixit();
let output = renderer.render(&diag, &sm);
assert!(output.contains(ansi::RED) || output.contains(ansi::GREEN));
}
#[test]
fn test_csv_renderer_with_header() {
let renderer = X86CsvRenderer::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let output = renderer.render(&diags);
assert!(output.contains("severity,file,line"));
assert!(output.contains("error"));
assert!(output.contains("warning"));
}
#[test]
fn test_csv_renderer_no_header() {
let mut renderer = X86CsvRenderer::new();
renderer.include_header = false;
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(!output.contains("severity,file,line"));
assert!(output.contains("error"));
}
#[test]
fn test_csv_renderer_empty() {
let renderer = X86CsvRenderer::new();
let diags: Vec<StructuredDiagnostic> = vec![];
let output = renderer.render(&diags);
assert!(output.contains("severity,file,line"));
}
#[test]
fn test_html_renderer_basic() {
let renderer = X86HtmlRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(output.contains("<!DOCTYPE html>"));
assert!(output.contains("error"));
assert!(output.contains("division by zero"));
}
#[test]
fn test_html_renderer_snippet_only() {
let mut renderer = X86HtmlRenderer::new();
renderer.full_document = false;
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(!output.contains("<!DOCTYPE html>"));
assert!(output.contains("clang-diag"));
}
#[test]
fn test_html_renderer_styles() {
let renderer = X86HtmlRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(output.contains("<style>"));
assert!(output.contains("font-family"));
}
#[test]
fn test_html_escape() {
assert_eq!(html_escape("<script>"), "<script>");
assert_eq!(html_escape("a & b"), "a & b");
assert_eq!(html_escape("\"quoted\""), ""quoted"");
assert_eq!(html_escape("'single'"), "'single'");
}
#[test]
fn test_yaml_renderer_basic() {
let renderer = X86YamlRenderer::new();
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(output.starts_with("---\n"));
assert!(output.contains("diagnostics:"));
assert!(output.contains("error"));
assert!(output.ends_with("...\n"));
}
#[test]
fn test_yaml_renderer_empty() {
let renderer = X86YamlRenderer::new();
let diags: Vec<StructuredDiagnostic> = vec![];
let output = renderer.render(&diags);
assert!(output.contains("total: 0"));
}
#[test]
fn test_yaml_renderer_with_notes() {
let renderer = X86YamlRenderer::new();
let diags = vec![make_test_diag_with_note()];
let output = renderer.render(&diags);
assert!(output.contains("notes:"));
}
#[test]
fn test_yaml_renderer_with_fixits() {
let renderer = X86YamlRenderer::new();
let diags = vec![make_test_diag_with_fixit()];
let output = renderer.render(&diags);
assert!(output.contains("fixits:"));
assert!(output.contains("replacement"));
}
#[test]
fn test_multiline_fixit_single_line() {
let ml = X86FixItMultiLine::new(8);
let source = "int x = 0 / 0;\n";
let fixit = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 10,
offset: 9 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 15,
offset: 14 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let result = ml.apply_fixits(source, &[fixit]);
assert!(result.is_ok());
assert!(result.unwrap().contains("int x = 1;"));
}
#[test]
fn test_multiline_fixit_conflict_detection() {
let ml = X86FixItMultiLine::new(8);
let source = "int x = 0 / 0;\n";
let fixit1 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 8,
offset: 7 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 12,
offset: 11 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let fixit2 = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 10,
offset: 9 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 14,
offset: 13 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "2".to_string(),
is_insertion: false,
};
let result = ml.apply_fixits(source, &[fixit1, fixit2]);
assert!(result.is_err());
}
#[test]
fn test_multiline_fixit_diff_output() {
let ml = X86FixItMultiLine::new(8);
let source = "int x = 0 / 0;\nint y = 1;\n";
let fixit = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 10,
offset: 9 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 15,
offset: 14 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let diff = ml.render_diff(source, &[fixit]);
assert!(diff.contains("-"));
assert!(diff.contains("+"));
}
#[test]
fn test_batch_processor_basic() {
let mut processor = X86DiagBatchProcessor::new(2);
let result1 = processor.add(make_test_diag());
assert!(result1.is_none());
let result2 = processor.add(make_test_warning_diag());
assert!(result2.is_some());
assert_eq!(result2.unwrap().len(), 2);
}
#[test]
fn test_batch_processor_flush() {
let mut processor = X86DiagBatchProcessor::new(10);
processor.add(make_test_diag());
let remaining = processor.flush();
assert!(remaining.is_some());
assert_eq!(remaining.unwrap().len(), 1);
}
#[test]
fn test_batch_processor_empty_flush() {
let mut processor = X86DiagBatchProcessor::new(10);
let remaining = processor.flush();
assert!(remaining.is_none());
}
#[test]
fn test_batch_processor_counts() {
let mut processor = X86DiagBatchProcessor::new(2);
assert_eq!(processor.batch_count(), 0);
assert_eq!(processor.pending_count(), 0);
processor.add(make_test_diag());
assert_eq!(processor.pending_count(), 1);
processor.add(make_test_warning_diag());
assert_eq!(processor.batch_count(), 1);
assert_eq!(processor.pending_count(), 0);
}
#[test]
fn test_severity_filter_escalation() {
let filter = X86DiagnosticSeverityFilter::strict();
let warn_diag = make_test_warning_diag();
let effective = filter.effective_severity(&warn_diag);
assert_eq!(effective, DiagSeverity::Error);
}
#[test]
fn test_severity_filter_apply() {
let filter = X86DiagnosticSeverityFilter::new(DiagSeverity::Warning);
let diags = vec![make_test_diag(), make_test_warning_diag()];
let result = filter.apply(&diags);
assert_eq!(result.len(), 2);
}
#[test]
fn test_severity_filter_remarks_as_warnings() {
let mut filter = X86DiagnosticSeverityFilter::new(DiagSeverity::Warning);
filter.remarks_as_warnings = true;
let mut remark = make_test_diag();
remark.severity = DiagSeverity::Remark;
let effective = filter.effective_severity(&remark);
assert_eq!(effective, DiagSeverity::Warning);
}
#[test]
fn test_sorter_by_severity() {
let sorter = X86DiagnosticSorter::new(X86DiagSortOrder::BySeverity);
let mut diags = vec![make_test_warning_diag(), make_test_diag()];
sorter.sort(&mut diags);
assert_eq!(diags[0].severity, DiagSeverity::Error);
assert_eq!(diags[1].severity, DiagSeverity::Warning);
}
#[test]
fn test_sorter_by_location() {
let sorter = X86DiagnosticSorter::new(X86DiagSortOrder::ByLocation);
let mut diags = vec![make_test_diag(), make_test_warning_diag()];
sorter.sort(&mut diags);
assert_eq!(diags[0].location.line, 2);
assert_eq!(diags[1].location.line, 3);
}
#[test]
fn test_sorter_original_preserves_order() {
let sorter = X86DiagnosticSorter::new(X86DiagSortOrder::Original);
let mut diags = vec![make_test_warning_diag(), make_test_diag()];
sorter.sort(&mut diags);
assert_eq!(diags[0].severity, DiagSeverity::Warning);
}
#[test]
fn test_pattern_matcher_glob() {
let mut matcher = X86DiagnosticPatternMatcher::new();
matcher.add_file_pattern("*.c");
matcher.set_inclusive(true);
let diag = make_test_diag();
assert!(matcher.matches(&diag));
}
#[test]
fn test_pattern_matcher_exact() {
let mut matcher = X86DiagnosticPatternMatcher::new();
matcher.add_file_pattern("test.c");
matcher.set_inclusive(true);
let diag = make_test_diag();
assert!(matcher.matches(&diag));
}
#[test]
fn test_pattern_matcher_message() {
let mut matcher = X86DiagnosticPatternMatcher::new();
matcher.add_message_pattern("division");
matcher.set_inclusive(true);
let diag = make_test_diag();
assert!(matcher.matches(&diag));
}
#[test]
fn test_pattern_matcher_exclusive() {
let mut matcher = X86DiagnosticPatternMatcher::new();
matcher.add_file_pattern("other.c");
matcher.set_inclusive(false);
let diag = make_test_diag();
assert!(matcher.matches(&diag));
}
#[test]
fn test_pattern_matcher_clear() {
let mut matcher = X86DiagnosticPatternMatcher::new();
matcher.add_file_pattern("*.c");
matcher.clear();
let diag = make_test_diag();
assert!(matcher.matches(&diag));
}
#[test]
fn test_source_chunk_render() {
let scheme = X86ColorScheme::monochrome();
let chunk = X86DiagSourceChunk {
file: "test.c".to_string(),
start_line: 2,
lines: vec![
(2, " int x = 0;".to_string()),
(3, " int y = x / 0;".to_string()),
(4, " return y;".to_string()),
],
diag_line: 3,
diag_column: 17,
highlights: vec![],
};
let output = chunk.render(&scheme, 8);
assert!(output.contains(">"));
assert!(output.contains("|"));
assert!(output.contains("^"));
}
#[test]
fn test_source_annotator_basic() {
let annotator = X86DiagSourceAnnotator::new(8, 1);
let sm = make_test_source_manager();
let diags = vec![make_test_diag()];
let chunks = annotator.annotate(&sm, &diags);
assert!(!chunks.is_empty());
assert_eq!(chunks[0].file, "test.c");
assert_eq!(chunks[0].diag_line, 3);
}
#[test]
fn test_source_annotator_rendered() {
let annotator = X86DiagSourceAnnotator::new(8, 1);
let sm = make_test_source_manager();
let diags = vec![make_test_diag()];
let scheme = X86ColorScheme::monochrome();
let output = annotator.render_annotated(&sm, &diags, &scheme);
assert!(output.contains(">"));
assert!(output.contains("|"));
}
#[test]
fn test_diag_context_from_diagnostic() {
let diag = make_test_diag_with_note();
let ctx = X86DiagContext::from_diagnostic(&diag);
assert!(ctx.primary.is_some());
assert_eq!(ctx.notes.len(), 1);
}
#[test]
fn test_diag_context_add_macro_step() {
let mut ctx = X86DiagContext::new();
ctx.add_macro_step(MacroExpansionStep {
macro_name: "TEST".to_string(),
definition_location: ClangSourceLocation::invalid(),
expansion_location: ClangSourceLocation::invalid(),
depth: 0,
source_context: None,
});
assert_eq!(ctx.macro_trace.len(), 1);
}
#[test]
fn test_diag_context_add_include_entry() {
let mut ctx = X86DiagContext::new();
ctx.add_include_entry(IncludeStackEntry {
file: "test.h".to_string(),
include_line: 1,
including_file: "main.c".to_string(),
depth: 0,
});
assert_eq!(ctx.include_chain.len(), 1);
}
#[test]
fn test_diag_context_add_template_note() {
let mut ctx = X86DiagContext::new();
ctx.add_template_note(TemplateInstantiationNote {
template_name: "Vec".to_string(),
definition_loc: ClangSourceLocation::invalid(),
instantiation_loc: ClangSourceLocation::invalid(),
depth: 0,
});
assert_eq!(ctx.template_trace.len(), 1);
}
#[test]
fn test_diag_context_render() {
let ctx = X86DiagContext::from_diagnostic(&make_test_diag());
let mut renderer = X86DiagRenderer::new();
let sm = make_test_source_manager();
let output = ctx.render(&mut renderer, &sm);
assert!(output.contains("division by zero"));
}
#[test]
fn test_pattern_glob_matching() {
let p = Pattern::new("*.c");
assert!(p.matches("test.c"));
assert!(p.matches("foo.c"));
assert!(!p.matches("test.h"));
}
#[test]
fn test_pattern_question_mark() {
let p = Pattern::new("test.?");
assert!(p.matches("test.c"));
assert!(p.matches("test.h"));
assert!(!p.matches("test.cc"));
}
#[test]
fn test_pattern_regex_like() {
let p = Pattern::new("/error/");
assert!(p.matches("this is an error message"));
assert!(!p.matches("this is a warning"));
}
#[test]
fn test_sorter_by_category() {
let sorter = X86DiagnosticSorter::new(X86DiagSortOrder::ByCategory);
let mut diags = vec![make_test_diag(), make_test_warning_diag()];
sorter.sort(&mut diags);
assert_eq!(diags[0].location.line, 2);
assert_eq!(diags[1].location.line, 3);
}
#[test]
fn test_multiline_fixit_default() {
let ml = X86FixItMultiLine::default();
assert_eq!(ml.tab_stop, 8);
assert!(ml.auto_merge);
}
#[test]
fn test_batch_processor_reset() {
let mut processor = X86DiagBatchProcessor::new(2);
processor.add(make_test_diag());
processor.reset();
assert_eq!(processor.batch_count(), 0);
assert_eq!(processor.pending_count(), 0);
}
#[test]
fn test_severity_filter_max_errors() {
let mut filter = X86DiagnosticSeverityFilter::new(DiagSeverity::Warning);
filter.max_errors = Some(1);
let diag1 = make_test_diag();
let mut diag2 = make_test_diag();
diag2.id = DiagID::ErrUndeclaredIdentifier;
let result = filter.apply(&[diag1, diag2]);
assert_eq!(result.len(), 1);
}
#[test]
fn test_source_chunk_with_highlights() {
let scheme = X86ColorScheme::monochrome();
let chunk = X86DiagSourceChunk {
file: "test.c".to_string(),
start_line: 3,
lines: vec![(3, " int y = x / 0;".to_string())],
diag_line: 3,
diag_column: 17,
highlights: vec![SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 17,
offset: 35 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 3,
column: 22,
offset: 40 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
}],
};
let output = chunk.render(&scheme, 8);
assert!(output.contains("~"));
}
#[test]
fn test_pipeline_compact_format() {
let mut pipeline = X86DiagnosticPipeline::new();
let diags = vec![make_test_diag()];
let result = pipeline.process(&diags, X86DiagOutputFormat::Compact, None);
assert!(result.output.contains("[ERROR]"));
}
#[test]
fn test_xml_renderer_multiline_message() {
let renderer = X86XmlRenderer::new();
let mut diag = make_test_diag();
diag.message = "test & <special> \"chars\"".to_string();
let output = renderer.render(&[diag]);
assert!(output.contains("&"));
assert!(output.contains("<"));
assert!(output.contains("""));
}
#[test]
fn test_csv_quoting() {
let mut renderer = X86CsvRenderer::new();
renderer.quote_all = true;
let mut diag = make_test_diag();
diag.message = "contains, comma".to_string();
let output = renderer.render(&[diag]);
assert!(output.contains("\"contains, comma\""));
}
#[test]
fn test_apply_fixit_hints_fn() {
let mut sm = ClangSourceManager::new();
let content = "int x = 0;\n";
let src_file = ClangSourceFile::new(Path::new("t.c"), &content);
sm.add_file(Path::new("t.c"), &src_file.content);
let fixit = FixItHint {
remove_range: SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("t.c"),
line: 1,
column: 8,
offset: 7 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("t.c"),
line: 1,
column: 11,
offset: 10 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
},
code: "1".to_string(),
is_insertion: false,
};
let result = apply_fixit_hints(&[fixit], &sm);
assert!(result.is_ok());
}
#[test]
fn test_detect_fixit_conflicts_fn() {
let fixit1 = FixItHint {
remove_range: SourceRange::new(
ClangSourceLocation {
file: PathBuf::from("t.c"),
line: 1,
column: 5,
offset: 4 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ClangSourceLocation {
file: PathBuf::from("t.c"),
line: 1,
column: 10,
offset: 9 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
),
code: "a".to_string(),
is_insertion: false,
};
let fixit2 = FixItHint {
remove_range: SourceRange::new(
ClangSourceLocation {
file: PathBuf::from("t.c"),
line: 1,
column: 8,
offset: 7 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ClangSourceLocation {
file: PathBuf::from("t.c"),
line: 1,
column: 12,
offset: 11 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
),
code: "b".to_string(),
is_insertion: false,
};
let conflicts = detect_fixit_conflicts(&[fixit1, fixit2]);
assert!(!conflicts.is_empty());
}
#[test]
fn test_convenience_functions() {
let sm = make_test_source_manager();
let output = render_diag_terminal(&make_test_diag(), &sm);
assert!(output.contains("error"));
let term_renderer = make_terminal_renderer(true);
assert!(term_renderer.options.is_tty);
let ide_renderer = make_ide_renderer();
assert!(!ide_renderer.options.show_carets);
let machine_renderer = make_machine_renderer();
assert!(!machine_renderer.options.show_fixits);
}
#[test]
fn test_sort_order_display_names() {
let orders = [
X86DiagSortOrder::ByLocation,
X86DiagSortOrder::BySeverity,
X86DiagSortOrder::ByDiagnosticID,
X86DiagSortOrder::ByCategory,
X86DiagSortOrder::Original,
];
assert_eq!(orders.len(), 5);
}
#[test]
fn test_color_mode_display() {
assert_eq!(ColorMode::Auto.to_string(), "auto");
assert_eq!(ColorMode::Always.to_string(), "always");
assert_eq!(ColorMode::Never.to_string(), "never");
}
#[test]
fn test_csv_renderer_delimiter() {
let mut renderer = X86CsvRenderer::new();
renderer.delimiter = ';';
let diags = vec![make_test_diag()];
let output = renderer.render(&diags);
assert!(output.contains("error;test.c"));
}
#[test]
fn test_unicode_edge_cases() {
assert_eq!(unicode_char_width('a'), 1);
assert_eq!(unicode_char_width(' '), 1);
assert_eq!(unicode_char_width('\u{200D}'), 0);
assert_eq!(unicode_char_width('\u{4E2D}'), 2);
assert_eq!(unicode_char_width('\u{FF21}'), 2);
}
#[test]
fn test_html_renderer_with_notes() {
let renderer = X86HtmlRenderer::new();
let diags = vec![make_test_diag_with_note()];
let output = renderer.render(&diags);
assert!(output.contains("note"));
assert!(output.contains("declared here"));
}
#[test]
fn test_html_renderer_with_fixits() {
let renderer = X86HtmlRenderer::new();
let diags = vec![make_test_diag_with_fixit()];
let output = renderer.render(&diags);
assert!(output.contains("Fix-Its"));
}
#[test]
fn test_pipeline_with_source_manager() {
let mut pipeline = X86DiagnosticPipeline::new();
let sm = make_test_source_manager();
let diags = vec![make_test_diag()];
let result = pipeline.process(&diags, X86DiagOutputFormat::Terminal, Some(&sm));
assert_eq!(result.error_count, 1);
}
#[test]
fn test_pipeline_stats_collection() {
let mut pipeline = X86DiagnosticPipeline::new();
let diags = vec![make_test_diag(), make_test_warning_diag()];
let result = pipeline.process(&diags, X86DiagOutputFormat::Gcc, None);
assert_eq!(result.stats.total, 2);
assert_eq!(result.stats.errors, 1);
assert_eq!(result.stats.warnings, 1);
}
}
#[derive(Debug, Clone, Default)]
pub struct X86CsvRenderer {
pub include_header: bool,
pub delimiter: char,
pub quote_all: bool,
}
impl X86CsvRenderer {
pub fn new() -> Self {
Self {
include_header: true,
delimiter: ',',
quote_all: false,
}
}
pub fn render(&self, diags: &[StructuredDiagnostic]) -> String {
let mut output = String::new();
if self.include_header {
output.push_str(&self.csv_row(&[
"severity",
"file",
"line",
"column",
"message",
"category",
"flag",
"fixit_count",
"note_count",
"range_count",
]));
}
for diag in diags {
let row = vec![
diag.severity.as_str().to_string(),
diag.location.file.to_string_lossy().to_string(),
diag.location.line.to_string(),
diag.location.column.to_string(),
diag.message.clone(),
diag.category.clone(),
diag.flag_name.clone().unwrap_or_default(),
diag.fixits.len().to_string(),
diag.notes.len().to_string(),
diag.ranges.len().to_string(),
];
output.push_str(&self.csv_row(&row.iter().map(|s| s.as_str()).collect::<Vec<_>>()));
}
output
}
fn csv_row(&self, fields: &[&str]) -> String {
let quoted: Vec<String> = fields
.iter()
.map(|f| {
let needs_quoting = self.quote_all
|| f.contains(self.delimiter)
|| f.contains('"')
|| f.contains('\n')
|| f.contains('\r');
if needs_quoting {
format!("\"{}\"", f.replace('"', "\"\""))
} else {
f.to_string()
}
})
.collect();
format!("{}\n", quoted.join(&self.delimiter.to_string()))
}
}
#[derive(Debug, Clone)]
pub struct X86HtmlRenderer {
pub full_document: bool,
pub title: String,
pub inline_styles: bool,
pub class_prefix: String,
}
impl X86HtmlRenderer {
pub fn new() -> Self {
Self {
full_document: true,
title: "Clang Diagnostics Report".to_string(),
inline_styles: true,
class_prefix: "clang-diag".to_string(),
}
}
pub fn render(&self, diags: &[StructuredDiagnostic]) -> String {
let mut html = String::new();
if self.full_document {
html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
html.push_str(&format!(
"<meta charset=\"UTF-8\">\n<title>{}</title>\n",
html_escape(&self.title)
));
if self.inline_styles {
html.push_str(&self.default_styles());
}
html.push_str("</head>\n<body>\n");
html.push_str(&format!(
"<h1 class=\"{}-title\">{}</h1>\n",
self.class_prefix,
html_escape(&self.title)
));
}
let errors: Vec<_> = diags.iter().filter(|d| d.severity.is_error()).collect();
let warnings: Vec<_> = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.collect();
let notes: Vec<_> = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Note)
.collect();
html.push_str(&format!("<div class=\"{}-summary\">\n", self.class_prefix));
html.push_str(&format!(
"<p>Total: {} | Errors: <span class=\"{}-error-count\">{}</span> | Warnings: <span class=\"{}-warn-count\">{}</span> | Notes: {}</p>\n",
diags.len(),
self.class_prefix,
errors.len(),
self.class_prefix,
warnings.len(),
notes.len()
));
html.push_str("</div>\n");
if !diags.is_empty() {
html.push_str(&format!("<div class=\"{}-entries\">\n", self.class_prefix));
for diag in diags {
html.push_str(&self.render_entry(diag));
}
html.push_str("</div>\n");
}
if self.full_document {
html.push_str("</body>\n</html>\n");
}
html
}
fn render_entry(&self, diag: &StructuredDiagnostic) -> String {
let severity_class = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => "error",
DiagSeverity::Warning => "warning",
DiagSeverity::Note => "note",
DiagSeverity::Remark => "remark",
DiagSeverity::Ignored => "ignored",
};
let mut entry = String::new();
entry.push_str(&format!(
"<div class=\"{} {} {}-{}\">\n",
self.class_prefix, severity_class, self.class_prefix, severity_class
));
entry.push_str(&format!("<div class=\"{}-header\">\n", self.class_prefix));
entry.push_str(&format!(
"<span class=\"{}-severity\">{}</span>\n",
self.class_prefix,
html_escape(diag.severity.as_str())
));
if diag.location.is_valid {
entry.push_str(&format!(
"<span class=\"{}-location\">{}:{}:{}</span>\n",
self.class_prefix,
html_escape(&diag.location.file.to_string_lossy()),
diag.location.line,
diag.location.column
));
}
entry.push_str(&format!(
"<span class=\"{}-message\">{}</span>\n",
self.class_prefix,
html_escape(&diag.message)
));
if let Some(ref flag) = diag.flag_name {
entry.push_str(&format!(
"<code class=\"{}-flag\">{}</code>\n",
self.class_prefix,
html_escape(flag)
));
}
entry.push_str("</div>\n");
if !diag.fixits.is_empty() {
entry.push_str(&format!("<div class=\"{}-fixits\">\n", self.class_prefix));
entry.push_str(&format!(
"<strong>Fix-Its ({}):</strong>\n",
diag.fixits.len()
));
entry.push_str("<ul>\n");
for fixit in &diag.fixits {
let action = if fixit.is_insertion {
format!("Insert \"{}\"", html_escape(&fixit.code))
} else if fixit.code.is_empty() {
"Remove".to_string()
} else {
format!("Replace with \"{}\"", html_escape(&fixit.code))
};
entry.push_str(&format!(
"<li>{} at {}:{}:{}</li>\n",
action,
fixit.remove_range.begin.line,
fixit.remove_range.begin.column,
fixit.remove_range.end.line
));
}
entry.push_str("</ul>\n");
entry.push_str("</div>\n");
}
for note in &diag.notes {
entry.push_str(&format!("<div class=\"{}-note\">\n", self.class_prefix));
entry.push_str(&format!(
"<span class=\"{}-note-label\">note:</span> {}\n",
self.class_prefix,
html_escape(¬e.message)
));
if note.location.is_valid {
entry.push_str(&format!(
" <span class=\"{}-note-loc\">at {}:{}:{}</span>\n",
self.class_prefix,
html_escape(¬e.location.file.to_string_lossy()),
note.location.line,
note.location.column
));
}
entry.push_str("</div>\n");
}
entry.push_str("</div>\n");
entry
}
fn default_styles(&self) -> String {
format!(
"<style>\n\
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
margin: 20px; background: #1e1e1e; color: #d4d4d4; }}\n\
h1.{p}-title {{ color: #569cd6; border-bottom: 1px solid #333; padding-bottom: 8px; }}\n\
.{p}-summary {{ background: #252526; padding: 12px; border-radius: 4px; margin-bottom: 16px; }}\n\
.{p}-error-count {{ color: #f44747; font-weight: bold; }}\n\
.{p}-warn-count {{ color: #cca700; font-weight: bold; }}\n\
.{p}-entries {{ font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace; }}\n\
.{p} {{ padding: 8px; margin: 4px 0; border-left: 4px solid #555; }}\n\
.{p}-error {{ border-left-color: #f44747; background: #2d1f1f; }}\n\
.{p}-warning {{ border-left-color: #cca700; background: #2d2a1c; }}\n\
.{p}-note {{ border-left-color: #569cd6; background: #1e2a35; }}\n\
.{p}-remark {{ border-left-color: #6a9955; background: #1e2d1e; }}\n\
.{p}-header {{ display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }}\n\
.{p}-severity {{ font-weight: bold; text-transform: uppercase; }}\n\
.{p}-error .{p}-severity {{ color: #f44747; }}\n\
.{p}-warning .{p}-severity {{ color: #cca700; }}\n\
.{p}-location {{ color: #808080; }}\n\
.{p}-message {{ flex: 1; }}\n\
.{p}-flag {{ color: #ce9178; }}\n\
.{p}-fixits {{ margin-top: 8px; padding-left: 16px; }}\n\
.{p}-note {{ margin: 4px 0 4px 24px; border-left-width: 2px; }}\n\
.{p}-note-label {{ font-weight: bold; color: #569cd6; }}\n\
.{p}-note-loc {{ color: #808080; }}\n\
</style>\n",
p = self.class_prefix
)
}
}
impl Default for X86HtmlRenderer {
fn default() -> Self {
Self::new()
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[derive(Debug, Clone, Default)]
pub struct X86YamlRenderer;
impl X86YamlRenderer {
pub fn new() -> Self {
Self
}
pub fn render(&self, diags: &[StructuredDiagnostic]) -> String {
let mut yaml = String::from("---\n");
yaml.push_str(&format!("total: {}\n", diags.len()));
let errors = diags.iter().filter(|d| d.severity.is_error()).count();
let warnings = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.count();
let notes = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Note)
.count();
yaml.push_str(&format!("errors: {}\n", errors));
yaml.push_str(&format!("warnings: {}\n", warnings));
yaml.push_str(&format!("notes: {}\n", notes));
if !diags.is_empty() {
yaml.push_str("diagnostics:\n");
for diag in diags {
yaml.push_str(&self.render_diag(diag, " "));
}
}
yaml.push_str("...\n");
yaml
}
fn render_diag(&self, diag: &StructuredDiagnostic, indent: &str) -> String {
let mut yaml = String::new();
let inner = format!("{} ", indent);
yaml.push_str(&format!(
"{}- severity: {}\n",
indent,
diag.severity.as_str()
));
yaml.push_str(&format!(
"{} message: \"{}\"\n",
indent,
yaml_escape(&diag.message)
));
yaml.push_str(&format!(
"{} category: \"{}\"\n",
indent,
yaml_escape(&diag.category)
));
if diag.location.is_valid {
yaml.push_str(&format!("{} location:\n", indent));
yaml.push_str(&format!(
"{} file: \"{}\"\n",
indent,
yaml_escape(&diag.location.file.to_string_lossy())
));
yaml.push_str(&format!("{} line: {}\n", indent, diag.location.line));
yaml.push_str(&format!("{} column: {}\n", indent, diag.location.column));
}
if let Some(ref flag) = diag.flag_name {
yaml.push_str(&format!("{} flag: \"{}\"\n", indent, yaml_escape(flag)));
}
if !diag.ranges.is_empty() {
yaml.push_str(&format!("{} ranges:\n", indent));
for range in &diag.ranges {
yaml.push_str(&format!(
"{} - file: \"{}\"\n",
indent,
yaml_escape(&range.begin.file.to_string_lossy())
));
yaml.push_str(&format!(
"{} start: [ {}, {} ]\n",
indent, range.begin.line, range.begin.column
));
yaml.push_str(&format!(
"{} end: [ {}, {} ]\n",
indent, range.end.line, range.end.column
));
}
}
if !diag.fixits.is_empty() {
yaml.push_str(&format!("{} fixits:\n", indent));
for fixit in &diag.fixits {
let action = if fixit.is_insertion {
"insertion"
} else if fixit.code.is_empty() {
"removal"
} else {
"replacement"
};
yaml.push_str(&format!("{} - action: {}\n", indent, action));
yaml.push_str(&format!(
"{} location: [ {}, {} ]\n",
indent, fixit.remove_range.begin.line, fixit.remove_range.begin.column
));
if !fixit.code.is_empty() {
yaml.push_str(&format!(
"{} code: \"{}\"\n",
indent,
yaml_escape(&fixit.code)
));
}
}
}
if !diag.notes.is_empty() {
yaml.push_str(&format!("{} notes:\n", indent));
for note in &diag.notes {
yaml.push_str(&format!(
"{} - message: \"{}\"\n",
indent,
yaml_escape(¬e.message)
));
if note.location.is_valid {
yaml.push_str(&format!(
"{} file: \"{}\"\n",
indent,
yaml_escape(¬e.location.file.to_string_lossy())
));
yaml.push_str(&format!("{} line: {}\n", indent, note.location.line));
yaml.push_str(&format!(
"{} column: {}\n",
indent, note.location.column
));
}
}
}
yaml
}
}
fn yaml_escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
#[derive(Debug, Clone)]
pub struct X86FixItMultiLine {
pub tab_stop: u32,
pub auto_merge: bool,
pub merge_gap: u32,
}
impl X86FixItMultiLine {
pub fn new(tab_stop: u32) -> Self {
Self {
tab_stop,
auto_merge: true,
merge_gap: 2,
}
}
pub fn apply_fixits(&self, source: &str, fixits: &[FixItHint]) -> Result<String, Vec<String>> {
let lines: Vec<&str> = source.lines().collect();
let mut edits: Vec<(usize, usize, usize, usize, String)> = Vec::new();
for fixit in fixits {
let start_line = fixit.remove_range.begin.line as usize;
let start_col = fixit.remove_range.begin.column as usize;
let end_line = fixit.remove_range.end.line as usize;
let end_col = fixit.remove_range.end.column as usize;
edits.push((start_line, start_col, end_line, end_col, fixit.code.clone()));
}
edits.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
edits.reverse();
let mut conflicts = Vec::new();
for i in 0..edits.len() {
for j in (i + 1)..edits.len() {
let a = &edits[i];
let b = &edits[j];
if a.0 == b.0 && ((a.1 <= b.1 && b.1 < a.3) || (b.1 <= a.1 && a.1 < b.3)) {
conflicts.push(format!(
"Fix-it conflict at line {}: column ranges {}-{} and {}-{} overlap",
a.0, a.1, a.3, b.1, b.3
));
}
}
}
if !conflicts.is_empty() {
return Err(conflicts);
}
let mut result = source.to_string();
edits.sort_by(|a, b| {
b.0.cmp(&a.0)
.then(b.1.cmp(&a.1))
.then(b.2.cmp(&a.2))
.then(b.3.cmp(&a.3))
});
for (start_line, start_col, end_line, end_col, replacement) in &edits {
result = self.replace_range(
&result,
*start_line,
*start_col,
*end_line,
*end_col,
replacement,
);
}
Ok(result)
}
fn replace_range(
&self,
source: &str,
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
replacement: &str,
) -> String {
let lines: Vec<&str> = source.lines().collect();
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
if line_num < start_line || line_num > end_line {
result.push_str(line);
result.push('\n');
} else if line_num == start_line && line_num == end_line {
let before = &line[..((start_col - 1).min(line.len()))];
let after = &line[(end_col - 1).min(line.len())..];
result.push_str(before);
result.push_str(replacement);
result.push_str(after);
result.push('\n');
} else if line_num == start_line {
let before = &line[..((start_col - 1).min(line.len()))];
result.push_str(before);
result.push_str(replacement);
} else if line_num == end_line {
let after = &line[(end_col - 1).min(line.len())..];
result.push_str(after);
result.push('\n');
}
}
if source.ends_with('\n') && !result.ends_with('\n') {
result.push('\n');
}
result
}
pub fn render_diff(&self, source: &str, fixits: &[FixItHint]) -> String {
if fixits.is_empty() {
return String::new();
}
let mut output = String::new();
let lines: Vec<&str> = source.lines().collect();
for fixit in fixits {
let start_line = fixit.remove_range.begin.line as usize;
let end_line = fixit.remove_range.end.line as usize;
let ctx_start = if start_line > 2 { start_line - 2 } else { 1 };
for l in ctx_start..start_line {
if l <= lines.len() {
output.push_str(&format!(" {}\n", lines[l - 1]));
}
}
for l in start_line..=end_line {
if l <= lines.len() {
let line = self.expand_tabs(lines[l - 1]);
if l == start_line && l == end_line {
let start_col = fixit.remove_range.begin.column as usize;
let end_col = fixit.remove_range.end.column as usize;
let before = &line[..((start_col - 1).min(line.len()))];
let removed =
&line[(start_col - 1).min(line.len())..(end_col - 1).min(line.len())];
let after = &line[(end_col - 1).min(line.len())..];
output.push_str(&format!("-{}|{}|{}\n", before, removed, after));
} else {
output.push_str(&format!("-{}\n", line));
}
}
}
for replacement_line in fixit.code.lines() {
output.push_str(&format!("+{}\n", self.expand_tabs(replacement_line)));
}
let ctx_end = (end_line + 2).min(lines.len());
for l in (end_line + 1)..=ctx_end {
if l <= lines.len() {
output.push_str(&format!(" {}\n", lines[l - 1]));
}
}
output.push('\n');
}
output
}
fn expand_tabs(&self, s: &str) -> String {
expand_tabs(s, self.tab_stop)
}
}
impl Default for X86FixItMultiLine {
fn default() -> Self {
Self::new(8)
}
}
#[derive(Debug, Clone)]
pub struct X86DiagBatchProcessor {
pub batch_size: usize,
accumulated: Vec<StructuredDiagnostic>,
batch_count: u32,
}
impl X86DiagBatchProcessor {
pub fn new(batch_size: usize) -> Self {
Self {
batch_size: batch_size.max(1),
accumulated: Vec::new(),
batch_count: 0,
}
}
pub fn add(&mut self, diag: StructuredDiagnostic) -> Option<Vec<StructuredDiagnostic>> {
self.accumulated.push(diag);
if self.accumulated.len() >= self.batch_size {
self.batch_count += 1;
Some(std::mem::take(&mut self.accumulated))
} else {
None
}
}
pub fn add_all(&mut self, diags: Vec<StructuredDiagnostic>) -> Vec<Vec<StructuredDiagnostic>> {
let mut batches = Vec::new();
for diag in diags {
if let Some(batch) = self.add(diag) {
batches.push(batch);
}
}
batches
}
pub fn flush(&mut self) -> Option<Vec<StructuredDiagnostic>> {
if self.accumulated.is_empty() {
None
} else {
self.batch_count += 1;
Some(std::mem::take(&mut self.accumulated))
}
}
pub fn batch_count(&self) -> u32 {
self.batch_count
}
pub fn pending_count(&self) -> usize {
self.accumulated.len()
}
pub fn reset(&mut self) {
self.accumulated.clear();
self.batch_count = 0;
}
}
impl Default for X86DiagBatchProcessor {
fn default() -> Self {
Self::new(100)
}
}
#[derive(Debug, Clone)]
pub struct X86DiagnosticSeverityFilter {
pub min_severity: DiagSeverity,
pub warnings_as_errors: bool,
pub remarks_as_warnings: bool,
pub escalate_notes_in_files: HashSet<String>,
pub max_errors: Option<u32>,
pub max_warnings: Option<u32>,
}
impl X86DiagnosticSeverityFilter {
pub fn new(min_severity: DiagSeverity) -> Self {
Self {
min_severity,
warnings_as_errors: false,
remarks_as_warnings: false,
escalate_notes_in_files: HashSet::new(),
max_errors: None,
max_warnings: None,
}
}
pub fn strict() -> Self {
Self {
min_severity: DiagSeverity::Warning,
warnings_as_errors: true,
remarks_as_warnings: true,
escalate_notes_in_files: HashSet::new(),
max_errors: None,
max_warnings: None,
}
}
pub fn effective_severity(&self, diag: &StructuredDiagnostic) -> DiagSeverity {
match diag.severity {
DiagSeverity::Warning if self.warnings_as_errors => DiagSeverity::Error,
DiagSeverity::Remark if self.remarks_as_warnings => DiagSeverity::Warning,
DiagSeverity::Note
if self
.escalate_notes_in_files
.contains(&*diag.location.file.to_string_lossy()) =>
{
DiagSeverity::Warning
}
other => other,
}
}
pub fn passes(&self, diag: &StructuredDiagnostic) -> bool {
let effective = self.effective_severity(diag);
X86DiagnosticFilter::severity_meets_min(&effective, &self.min_severity)
}
pub fn apply<'a>(&self, diags: &'a [StructuredDiagnostic]) -> Vec<&'a StructuredDiagnostic> {
let mut result = Vec::new();
let mut error_count = 0u32;
let mut warn_count = 0u32;
for diag in diags {
if !self.passes(diag) {
continue;
}
let effective = self.effective_severity(diag);
match effective {
DiagSeverity::Error | DiagSeverity::Fatal => {
error_count += 1;
if let Some(max) = self.max_errors {
if error_count > max {
break;
}
}
}
DiagSeverity::Warning => {
warn_count += 1;
if let Some(max) = self.max_warnings {
if warn_count > max {
continue;
}
}
}
_ => {}
}
result.push(diag);
}
result
}
}
impl Default for X86DiagnosticSeverityFilter {
fn default() -> Self {
Self::new(DiagSeverity::Warning)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum X86DiagSortOrder {
ByLocation,
BySeverity,
ByDiagnosticID,
ByCategory,
#[default]
Original,
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagnosticSorter {
pub order: X86DiagSortOrder,
}
impl X86DiagnosticSorter {
pub fn new(order: X86DiagSortOrder) -> Self {
Self { order }
}
pub fn sort(&self, diags: &mut [StructuredDiagnostic]) {
match self.order {
X86DiagSortOrder::ByLocation => {
diags.sort_by(|a, b| {
a.location
.file
.cmp(&b.location.file)
.then(a.location.line.cmp(&b.location.line))
.then(a.location.column.cmp(&b.location.column))
});
}
X86DiagSortOrder::BySeverity => {
diags.sort_by(|a, b| {
let rank = |s: &DiagSeverity| -> u8 {
match s {
DiagSeverity::Fatal => 5,
DiagSeverity::Error => 4,
DiagSeverity::Warning => 3,
DiagSeverity::Note => 2,
DiagSeverity::Remark => 1,
DiagSeverity::Ignored => 0,
}
};
rank(&b.severity)
.cmp(&rank(&a.severity))
.then(a.location.file.cmp(&b.location.file))
.then(a.location.line.cmp(&b.location.line))
});
}
X86DiagSortOrder::ByDiagnosticID => {
diags.sort_by(|a, b| format!("{:?}", a.id).cmp(&format!("{:?}", b.id)));
}
X86DiagSortOrder::ByCategory => {
diags.sort_by(|a, b| {
a.category
.cmp(&b.category)
.then(a.location.file.cmp(&b.location.file))
.then(a.location.line.cmp(&b.location.line))
});
}
X86DiagSortOrder::Original => {
}
}
}
pub fn sorted(&self, diags: &[StructuredDiagnostic]) -> Vec<StructuredDiagnostic> {
let mut copy = diags.to_vec();
self.sort(&mut copy);
copy
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagnosticPatternMatcher {
file_patterns: Vec<Pattern>,
message_patterns: Vec<Pattern>,
inclusive: bool,
}
#[derive(Debug, Clone)]
struct Pattern {
raw: String,
is_glob: bool,
is_regex: bool,
}
impl Pattern {
fn new(raw: &str) -> Self {
let is_glob = raw.contains('*') || raw.contains('?');
let is_regex = raw.starts_with('/') && raw.ends_with('/');
Self {
raw: raw.to_string(),
is_glob,
is_regex,
}
}
fn matches(&self, text: &str) -> bool {
if self.is_regex {
let regex_str = &self.raw[1..self.raw.len() - 1];
text.contains(regex_str)
} else if self.is_glob {
Self::glob_match(&self.raw, text)
} else {
text.contains(&self.raw)
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pat_chars: Vec<char> = pattern.chars().collect();
let txt_chars: Vec<char> = text.chars().collect();
let mut dp = vec![vec![false; txt_chars.len() + 1]; pat_chars.len() + 1];
dp[0][0] = true;
for i in 1..=pat_chars.len() {
if pat_chars[i - 1] == '*' {
dp[i][0] = dp[i - 1][0];
}
}
for i in 1..=pat_chars.len() {
for j in 1..=txt_chars.len() {
if pat_chars[i - 1] == '*' {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
} else if pat_chars[i - 1] == '?' || pat_chars[i - 1] == txt_chars[j - 1] {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
dp[pat_chars.len()][txt_chars.len()]
}
}
impl X86DiagnosticPatternMatcher {
pub fn new() -> Self {
Self::default()
}
pub fn add_file_pattern(&mut self, pattern: &str) {
self.file_patterns.push(Pattern::new(pattern));
}
pub fn add_message_pattern(&mut self, pattern: &str) {
self.message_patterns.push(Pattern::new(pattern));
}
pub fn set_inclusive(&mut self, inclusive: bool) {
self.inclusive = inclusive;
}
pub fn matches(&self, diag: &StructuredDiagnostic) -> bool {
let file_match = if self.file_patterns.is_empty() {
true
} else {
self.file_patterns
.iter()
.any(|p| p.matches(&diag.location.file.to_string_lossy()))
};
let msg_match = if self.message_patterns.is_empty() {
true
} else {
self.message_patterns
.iter()
.any(|p| p.matches(&diag.message))
};
if self.inclusive {
file_match && msg_match
} else {
!(file_match || msg_match)
}
}
pub fn clear(&mut self) {
self.file_patterns.clear();
self.message_patterns.clear();
}
}
#[derive(Debug, Clone)]
pub struct X86DiagSourceChunk {
pub file: String,
pub start_line: u32,
pub lines: Vec<(u32, String)>,
pub diag_line: u32,
pub diag_column: u32,
pub highlights: Vec<SourceRange>,
}
impl X86DiagSourceChunk {
pub fn render(&self, color_scheme: &X86ColorScheme, tab_stop: u32) -> String {
let mut output = String::new();
let line_num_width = self
.lines
.last()
.map(|(n, _)| n.to_string().len())
.unwrap_or(4)
.max(4);
for (line_num, line) in &self.lines {
let marker = if *line_num == self.diag_line {
">"
} else {
" "
};
let expanded = expand_tabs(line, tab_stop);
writeln!(
output,
"{} {} | {}",
color_scheme.line_number(&format!(
"{}{:>width$}",
marker,
line_num,
width = line_num_width
)),
color_scheme.line_number("|"),
&expanded
)
.unwrap();
if *line_num == self.diag_line {
let caret_line = self.build_caret_line(&expanded, tab_stop);
writeln!(
output,
" {:>width$} | {}",
"",
color_scheme.caret(&caret_line),
width = line_num_width
)
.unwrap();
}
}
output
}
fn build_caret_line(&self, source_line: &str, tab_stop: u32) -> String {
let col = unicode_column_width_before(source_line, self.diag_column, tab_stop);
let mut caret = vec![' '; col.max(1)];
if col > 0 {
caret[col - 1] = '^';
} else {
caret.push('^');
}
for range in &self.highlights {
if range.begin.line == range.end.line && range.begin.column != range.end.column {
let start_col = unicode_column_width_before(
source_line,
range.begin.column.min(range.end.column),
tab_stop,
);
let end_col = unicode_column_width_before(
source_line,
range.begin.column.max(range.end.column),
tab_stop,
);
let begin = start_col.max(col);
let end = end_col.min(source_line.len());
if end > caret.len() {
caret.resize(end, '~');
}
for i in begin..end {
if i < caret.len() && (caret[i] == ' ' || caret[i] == '\0') {
caret[i] = '~';
}
}
}
}
let result: String = caret.into_iter().collect();
result.trim_end().to_string()
}
}
#[derive(Debug, Clone)]
pub struct X86DiagSourceAnnotator {
pub tab_stop: u32,
pub context_lines: u32,
}
impl X86DiagSourceAnnotator {
pub fn new(tab_stop: u32, context_lines: u32) -> Self {
Self {
tab_stop,
context_lines,
}
}
pub fn annotate(
&self,
source_manager: &ClangSourceManager,
diags: &[StructuredDiagnostic],
) -> Vec<X86DiagSourceChunk> {
let mut chunks = Vec::new();
let mut line_cache = SourceLineCache::new(self.tab_stop);
for diag in diags {
if !diag.location.is_valid {
continue;
}
let start_line = if diag.location.line > self.context_lines {
diag.location.line - self.context_lines
} else {
1u32
};
let end_line = diag.location.line + self.context_lines;
let mut lines = Vec::new();
for l in start_line..=end_line {
if let Some(content) =
line_cache.get_line(source_manager, diag.location.file.as_path(), l)
{
lines.push((l, content));
}
}
if !lines.is_empty() {
chunks.push(X86DiagSourceChunk {
file: diag.location.file.to_string_lossy().to_string(),
start_line,
lines,
diag_line: diag.location.line,
diag_column: diag.location.column,
highlights: diag.ranges.clone(),
});
}
}
chunks
}
pub fn render_annotated(
&self,
source_manager: &ClangSourceManager,
diags: &[StructuredDiagnostic],
color_scheme: &X86ColorScheme,
) -> String {
let chunks = self.annotate(source_manager, diags);
let mut output = String::new();
for chunk in &chunks {
if !output.is_empty() {
output.push_str("---\n");
}
let rendered = chunk.render(color_scheme, self.tab_stop);
output.push_str(&rendered);
}
output
}
}
impl Default for X86DiagSourceAnnotator {
fn default() -> Self {
Self::new(8, 2)
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagContext {
pub primary: Option<StructuredDiagnostic>,
pub notes: Vec<DiagnosticNote>,
pub fixits: Vec<FixItHint>,
pub ranges: Vec<SourceRange>,
pub macro_trace: Vec<MacroExpansionStep>,
pub include_chain: Vec<IncludeStackEntry>,
pub template_trace: Vec<TemplateInstantiationNote>,
pub context_messages: Vec<String>,
}
impl X86DiagContext {
pub fn new() -> Self {
Self::default()
}
pub fn from_diagnostic(diag: &StructuredDiagnostic) -> Self {
Self {
primary: Some(diag.clone()),
notes: diag.notes.clone(),
fixits: diag.fixits.clone(),
ranges: diag.ranges.clone(),
macro_trace: Vec::new(),
include_chain: Vec::new(),
template_trace: Vec::new(),
context_messages: Vec::new(),
}
}
pub fn add_macro_step(&mut self, step: MacroExpansionStep) {
self.macro_trace.push(step);
}
pub fn add_include_entry(&mut self, entry: IncludeStackEntry) {
self.include_chain.push(entry);
}
pub fn add_template_note(&mut self, note: TemplateInstantiationNote) {
self.template_trace.push(note);
}
pub fn add_context(&mut self, msg: &str) {
self.context_messages.push(msg.to_string());
}
pub fn render(
&self,
renderer: &mut X86DiagRenderer,
source_manager: &ClangSourceManager,
) -> String {
let mut output = String::new();
if !self.include_chain.is_empty() {
let include_text = renderer.render_include_stack(&self.include_chain);
output.push_str(&include_text);
}
if !self.macro_trace.is_empty() {
let macro_text = renderer.render_macro_backtrace(&self.macro_trace, source_manager);
output.push_str(¯o_text);
}
if !self.template_trace.is_empty() {
let bt = X86TemplateBacktrace::new(10);
let template_text = bt.render(&self.template_trace);
output.push_str(&template_text);
}
if let Some(ref diag) = self.primary {
let diag_text = renderer.render_terminal(&[diag.clone()], source_manager);
output.push_str(&diag_text);
}
for msg in &self.context_messages {
output.push_str(&format!(" note: {}\n", msg));
}
output
}
}
#[derive(Debug, Clone)]
pub struct X86MarkdownRenderer {
pub include_toc: bool,
pub collapsible_sections: bool,
pub use_emojis: bool,
}
impl X86MarkdownRenderer {
pub fn new() -> Self {
Self {
include_toc: false,
collapsible_sections: true,
use_emojis: true,
}
}
pub fn render(&self, diags: &[StructuredDiagnostic]) -> String {
let mut md = String::new();
md.push_str("# Clang Diagnostics Report\n\n");
let errors = diags.iter().filter(|d| d.severity.is_error()).count();
let warnings = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.count();
let notes = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Note)
.count();
md.push_str("| Type | Count |\n");
md.push_str("|------|-------|\n");
md.push_str(&format!(
"| {} Errors | {} |\n",
self.emoji("\u{274C}"),
errors
));
md.push_str(&format!(
"| {} Warnings | {} |\n",
self.emoji("\u{26A0}\u{FE0F}"),
warnings
));
md.push_str(&format!(
"| {} Notes | {} |\n",
self.emoji("\u{2139}\u{FE0F}"),
notes
));
md.push_str(&format!("| **Total** | **{}** |\n\n", diags.len()));
if self.include_toc && !diags.is_empty() {
md.push_str("## Table of Contents\n\n");
for (i, diag) in diags.iter().enumerate() {
let summary = &diag.message[..diag.message.len().min(60)];
md.push_str(&format!(
"- [Diagnostic {}](#diagnostic-{}) — {}\n",
i + 1,
i + 1,
summary
));
}
md.push('\n');
}
for (i, diag) in diags.iter().enumerate() {
md.push_str(&self.render_diag(diag, i + 1));
}
md
}
fn render_diag(&self, diag: &StructuredDiagnostic, index: usize) -> String {
let mut md = String::new();
let severity_icon = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => self.emoji("\u{274C}"),
DiagSeverity::Warning => self.emoji("\u{26A0}\u{FE0F}"),
DiagSeverity::Note => self.emoji("\u{2139}\u{FE0F}"),
DiagSeverity::Remark => self.emoji("\u{1F4DD}"),
DiagSeverity::Ignored => self.emoji("\u{1F6AB}"),
};
md.push_str(&format!("## Diagnostic {} {}\n\n", index, severity_icon));
let severity_badge = match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => "`ERROR`",
DiagSeverity::Warning => "`WARNING`",
DiagSeverity::Note => "`NOTE`",
DiagSeverity::Remark => "`REMARK`",
DiagSeverity::Ignored => "`IGNORED`",
};
md.push_str(&format!("**Severity:** {}\n\n", severity_badge));
md.push_str(&format!("**Message:** {}\n\n", diag.message));
if diag.location.is_valid {
md.push_str(&format!(
"**Location:** `{}:{}:{}`\n\n",
diag.location.file.display(),
diag.location.line,
diag.location.column
));
}
md.push_str(&format!("**Category:** {}\n\n", diag.category));
if let Some(ref flag) = diag.flag_name {
md.push_str(&format!("**Flag:** `{}`\n\n", flag));
}
if !diag.ranges.is_empty() {
md.push_str("**Source Ranges:**\n\n");
for range in &diag.ranges {
if range.is_valid() {
md.push_str(&format!(
"- `{}:{}:{}` to `{}:{}:{}`\n",
range.begin.file.display(),
range.begin.line,
range.begin.column,
range.end.file.display(),
range.end.line,
range.end.column
));
}
}
md.push('\n');
}
if !diag.fixits.is_empty() {
if self.collapsible_sections {
md.push_str(&format!(
"<details>\n<summary>\u{1F527} Fix-Its ({})</summary>\n\n",
diag.fixits.len()
));
} else {
md.push_str(&format!(
"### \u{1F527} Fix-Its ({})\n\n",
diag.fixits.len()
));
}
for (j, fixit) in diag.fixits.iter().enumerate() {
let action = if fixit.is_insertion {
"Insert"
} else if fixit.code.is_empty() {
"Remove"
} else {
"Replace"
};
md.push_str(&format!("**{} {}:** ", j + 1, action));
md.push_str(&format!(
"at `{}:{}:{}`\n",
fixit.remove_range.begin.file.display(),
fixit.remove_range.begin.line,
fixit.remove_range.begin.column
));
if !fixit.code.is_empty() {
md.push_str(&format!("```cpp\n{}\n```\n", fixit.code));
}
}
if self.collapsible_sections {
md.push_str("</details>\n\n");
}
}
if !diag.notes.is_empty() {
if self.collapsible_sections {
md.push_str(&format!(
"<details>\n<summary>\u{1F4DD} Notes ({})</summary>\n\n",
diag.notes.len()
));
} else {
md.push_str(&format!("### \u{1F4DD} Notes ({})\n\n", diag.notes.len()));
}
for note in &diag.notes {
md.push_str(&format!("> **Note:** {}\n", note.message));
if note.location.is_valid {
md.push_str(&format!(
"> at `{}:{}:{}`\n",
note.location.file.display(),
note.location.line,
note.location.column
));
}
md.push_str("> \n");
}
if self.collapsible_sections {
md.push_str("</details>\n\n");
}
}
md.push_str("---\n\n");
md
}
fn emoji<'a>(&self, e: &'a str) -> &'a str {
if self.use_emojis {
e
} else {
""
}
}
}
impl Default for X86MarkdownRenderer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagTransformer {
rules: Vec<TransformRule>,
}
#[derive(Debug, Clone)]
struct TransformRule {
id_pattern: Option<String>,
msg_pattern: Option<String>,
file_pattern: Option<String>,
new_severity: Option<DiagSeverity>,
append_message: Option<String>,
strip_fixits: bool,
strip_notes: bool,
}
impl X86DiagTransformer {
pub fn new() -> Self {
Self::default()
}
pub fn promote_warnings_to_errors(&mut self, msg_pattern: &str) {
self.rules.push(TransformRule {
id_pattern: None,
msg_pattern: Some(msg_pattern.to_string()),
file_pattern: None,
new_severity: Some(DiagSeverity::Error),
append_message: None,
strip_fixits: false,
strip_notes: false,
});
}
pub fn demote_errors_to_warnings(&mut self, msg_pattern: &str) {
self.rules.push(TransformRule {
id_pattern: None,
msg_pattern: Some(msg_pattern.to_string()),
file_pattern: None,
new_severity: Some(DiagSeverity::Warning),
append_message: None,
strip_fixits: false,
strip_notes: false,
});
}
pub fn append_message(&mut self, msg_pattern: &str, append: &str) {
self.rules.push(TransformRule {
id_pattern: None,
msg_pattern: Some(msg_pattern.to_string()),
file_pattern: None,
new_severity: None,
append_message: Some(append.to_string()),
strip_fixits: false,
strip_notes: false,
});
}
pub fn strip_fixits(&mut self, msg_pattern: &str) {
self.rules.push(TransformRule {
id_pattern: None,
msg_pattern: Some(msg_pattern.to_string()),
file_pattern: None,
new_severity: None,
append_message: None,
strip_fixits: true,
strip_notes: false,
});
}
pub fn transform(&self, diag: &mut StructuredDiagnostic) {
for rule in &self.rules {
let matches = self.rule_matches(rule, diag);
if !matches {
continue;
}
if let Some(ref new_sev) = rule.new_severity {
diag.severity = new_sev.clone();
}
if let Some(ref append) = rule.append_message {
diag.message = format!("{} {}", diag.message, append);
}
if rule.strip_fixits {
diag.fixits.clear();
}
if rule.strip_notes {
diag.notes.clear();
}
}
}
pub fn transform_all(&self, diags: &mut [StructuredDiagnostic]) {
for diag in diags.iter_mut() {
self.transform(diag);
}
}
fn rule_matches(&self, rule: &TransformRule, diag: &StructuredDiagnostic) -> bool {
if let Some(ref id_pat) = rule.id_pattern {
if !format!("{:?}", diag.id).contains(id_pat.as_str()) {
return false;
}
}
if let Some(ref msg_pat) = rule.msg_pattern {
if !diag.message.contains(msg_pat.as_str()) {
return false;
}
}
if let Some(ref file_pat) = rule.file_pattern {
if !diag
.location
.file
.to_string_lossy()
.contains(file_pat.as_str())
{
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct X86DiagnosticProgress {
pub total: u64,
pub processed: u64,
pub errors: u64,
pub warnings: u64,
pub files_processed: u64,
pub current_file: Option<String>,
}
impl X86DiagnosticProgress {
pub fn new(total: u64) -> Self {
Self {
total,
processed: 0,
errors: 0,
warnings: 0,
files_processed: 0,
current_file: None,
}
}
pub fn advance(&mut self, diags: &[StructuredDiagnostic]) {
self.processed += diags.len() as u64;
self.errors += diags.iter().filter(|d| d.severity.is_error()).count() as u64;
self.warnings += diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.count() as u64;
}
pub fn file_done(&mut self) {
self.files_processed += 1;
}
pub fn percentage(&self) -> f64 {
if self.total == 0 {
return 100.0;
}
(self.processed as f64 / self.total as f64) * 100.0
}
pub fn format_progress(&self) -> String {
let pct = self.percentage();
let bar_width = 30;
let filled = ((pct / 100.0) * bar_width as f64) as usize;
let empty = bar_width - filled;
let bar = format!("[{}{}] {:.1}%", "=".repeat(filled), " ".repeat(empty), pct);
let file_info = self
.current_file
.as_ref()
.map(|f| format!(" {}", f))
.unwrap_or_default();
format!(
"{} ({}/{}) E:{} W:{} F:{}{}",
bar,
self.processed,
self.total,
self.errors,
self.warnings,
self.files_processed,
file_info
)
}
pub fn reset(&mut self) {
self.processed = 0;
self.errors = 0;
self.warnings = 0;
self.files_processed = 0;
self.current_file = None;
}
}
#[derive(Debug, Clone)]
pub struct X86FixedDiagRenderer {
pub format: X86DiagOutputFormat,
pub options: X86DiagnosticOptions,
}
impl X86FixedDiagRenderer {
pub fn new(format: X86DiagOutputFormat) -> Self {
Self {
format,
options: X86DiagnosticOptions::for_machine(),
}
}
pub fn render(&self, diags: &[StructuredDiagnostic]) -> String {
match self.format {
X86DiagOutputFormat::Gcc => X86GccRenderer::new().render_all(diags),
X86DiagOutputFormat::Msvc => X86MsvcRenderer::new().render_all(diags),
X86DiagOutputFormat::Vi => X86ViRenderer::new().render_all(diags),
X86DiagOutputFormat::Json => X86JsonRenderer::new().render_diagnostics(diags),
X86DiagOutputFormat::Compact => X86DiagnosticCompactor::new().compact_all(diags),
_ => {
X86GccRenderer::new().render_all(diags)
}
}
}
pub fn render_single(&self, diag: &StructuredDiagnostic) -> String {
self.render(&[diag.clone()])
}
}
#[derive(Debug, Clone, Default)]
pub struct X86DiagMetrics {
pub error_density: f64,
pub warning_density: f64,
pub fixit_coverage: f64,
pub top_category: Option<String>,
pub top_flag: Option<String>,
pub diags_per_file: HashMap<String, u32>,
}
impl X86DiagMetrics {
pub fn compute(diags: &[StructuredDiagnostic], total_lines: u64) -> Self {
let error_count = diags.iter().filter(|d| d.severity.is_error()).count() as f64;
let warn_count = diags
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
.count() as f64;
let fixit_count = diags.iter().filter(|d| !d.fixits.is_empty()).count() as f64;
let total = diags.len() as f64;
let error_density = if total_lines > 0 {
error_count / total_lines as f64
} else {
0.0
};
let warning_density = if total_lines > 0 {
warn_count / total_lines as f64
} else {
0.0
};
let fixit_coverage = if total > 0.0 {
(fixit_count / total) * 100.0
} else {
0.0
};
let mut cat_freq: HashMap<String, u32> = HashMap::new();
for diag in diags {
*cat_freq.entry(diag.category.clone()).or_insert(0) += 1;
}
let top_category = cat_freq
.iter()
.max_by_key(|(_, c)| *c)
.map(|(k, _)| k.clone());
let mut flag_freq: HashMap<String, u32> = HashMap::new();
for diag in diags {
if let Some(ref flag) = diag.flag_name {
*flag_freq.entry(flag.clone()).or_insert(0) += 1;
}
}
let top_flag = flag_freq
.iter()
.max_by_key(|(_, c)| *c)
.map(|(k, _)| k.clone());
let mut diags_per_file: HashMap<String, u32> = HashMap::new();
for diag in diags {
*diags_per_file
.entry(diag.location.file.to_string_lossy().to_string())
.or_insert(0) += 1;
}
Self {
error_density,
warning_density,
fixit_coverage,
top_category,
top_flag,
diags_per_file,
}
}
pub fn format_report(&self) -> String {
let mut report = String::new();
writeln!(report, "=== Diagnostic Quality Metrics ===").unwrap();
writeln!(
report,
"Error density: {:.4} errors/line",
self.error_density
)
.unwrap();
writeln!(
report,
"Warning density: {:.4} warnings/line",
self.warning_density
)
.unwrap();
writeln!(report, "Fix-it coverage: {:.1}%", self.fixit_coverage).unwrap();
if let Some(ref cat) = self.top_category {
writeln!(report, "Top category: {}", cat).unwrap();
}
if let Some(ref flag) = self.top_flag {
writeln!(report, "Top flag: {}", flag).unwrap();
}
if !self.diags_per_file.is_empty() {
writeln!(report, "\nDiagnostics per file:").unwrap();
let mut files: Vec<_> = self.diags_per_file.iter().collect();
files.sort_by(|a, b| b.1.cmp(a.1));
for (file, count) in files.iter().take(10) {
writeln!(report, " {}: {}", file, count).unwrap();
}
}
report
}
}
#[derive(Debug, Clone)]
pub enum X86DiagPreset {
VerboseTerminal,
CompactLines,
IdeIntegration,
CiPipeline,
CodeReview,
StatisticsOnly,
}
impl X86DiagPreset {
pub fn create_renderer(&self) -> X86DiagRenderer {
match self {
X86DiagPreset::VerboseTerminal => {
let mut opts = X86DiagnosticOptions::for_terminal();
opts.context_lines_before = 2;
opts.context_lines_after = 2;
opts.show_macro_backtrace = true;
opts.show_include_stack = true;
opts.show_template_backtrace = true;
X86DiagRenderer::with_options(opts)
}
X86DiagPreset::CompactLines => {
let mut opts = X86DiagnosticOptions::for_terminal();
opts.show_carets = false;
opts.show_fixits = false;
opts.show_source_ranges = false;
opts.context_lines_before = 0;
opts.context_lines_after = 0;
X86DiagRenderer::with_options(opts)
}
X86DiagPreset::IdeIntegration => {
let opts = X86DiagnosticOptions::for_ide();
X86DiagRenderer::with_options(opts)
}
X86DiagPreset::CiPipeline => {
let mut opts = X86DiagnosticOptions::for_machine();
opts.color_mode = ColorMode::Never;
opts.show_option_names = true;
X86DiagRenderer::with_options(opts)
}
X86DiagPreset::CodeReview => {
let mut opts = X86DiagnosticOptions::for_terminal();
opts.show_fixits = true;
opts.show_carets = true;
opts.context_lines_before = 1;
opts.context_lines_after = 1;
opts.show_macro_backtrace = false;
opts.show_include_stack = false;
let mut renderer = X86DiagRenderer::with_options(opts);
renderer.set_filter(X86DiagnosticFilter::new().min_severity(DiagSeverity::Warning));
renderer
}
X86DiagPreset::StatisticsOnly => {
let mut opts = X86DiagnosticOptions::for_machine();
opts.show_carets = false;
opts.show_fixits = false;
X86DiagRenderer::with_options(opts)
}
}
}
pub fn name(&self) -> &'static str {
match self {
X86DiagPreset::VerboseTerminal => "verbose-terminal",
X86DiagPreset::CompactLines => "compact-lines",
X86DiagPreset::IdeIntegration => "ide-integration",
X86DiagPreset::CiPipeline => "ci-pipeline",
X86DiagPreset::CodeReview => "code-review",
X86DiagPreset::StatisticsOnly => "statistics-only",
}
}
pub fn all() -> Vec<X86DiagPreset> {
vec![
X86DiagPreset::VerboseTerminal,
X86DiagPreset::CompactLines,
X86DiagPreset::IdeIntegration,
X86DiagPreset::CiPipeline,
X86DiagPreset::CodeReview,
X86DiagPreset::StatisticsOnly,
]
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"verbose-terminal" | "verbose" => Some(X86DiagPreset::VerboseTerminal),
"compact-lines" | "compact" => Some(X86DiagPreset::CompactLines),
"ide-integration" | "ide" => Some(X86DiagPreset::IdeIntegration),
"ci-pipeline" | "ci" => Some(X86DiagPreset::CiPipeline),
"code-review" | "review" => Some(X86DiagPreset::CodeReview),
"statistics-only" | "stats" => Some(X86DiagPreset::StatisticsOnly),
_ => None,
}
}
}
#[doc(hidden)]
pub fn _doc_example() {}
#[cfg(test)]
mod extended_tests {
use super::*;
use crate::clang::diagnostics::{
ClangSourceFile, ClangSourceLocation, ClangSourceManager, DiagID, DiagSeverity,
DiagnosticNote, FixItHint, SourceRange, StructuredDiagnostic,
};
fn make_test_sm() -> ClangSourceManager {
let mut sm = ClangSourceManager::new();
let content = "int main() {\n int x = 0;\n return x;\n}\n";
sm.add_file(Path::new("test.c"), &content);
sm
}
fn make_err_diag() -> StructuredDiagnostic {
StructuredDiagnostic {
id: DiagID::ErrDivisionByZeroConst,
severity: DiagSeverity::Error,
message: "division by zero".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 2,
column: 13,
offset: 25 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
notes: vec![],
fixits: vec![],
flag_name: Some("-Wdiv-zero".to_string()),
category: "Semantic".to_string(),
}
}
fn make_warn_diag() -> StructuredDiagnostic {
StructuredDiagnostic {
id: DiagID::WUnusedVariable,
severity: DiagSeverity::Warning,
message: "unused variable".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 2,
column: 9,
offset: 20 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
notes: vec![],
fixits: vec![],
flag_name: Some("-Wunused-variable".to_string()),
category: "Semantic".to_string(),
}
}
#[test]
fn test_markdown_renderer_basic() {
let renderer = X86MarkdownRenderer::new();
let diags = vec![make_err_diag()];
let output = renderer.render(&diags);
assert!(output.contains("# Clang Diagnostics Report"));
assert!(output.contains("division by zero"));
assert!(output.contains("ERROR"));
}
#[test]
fn test_markdown_renderer_with_notes() {
let renderer = X86MarkdownRenderer::new();
let mut diag = make_err_diag();
diag.notes.push(DiagnosticNote {
message: "declared here".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 1,
offset: 0 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
});
let output = renderer.render(&[diag]);
assert!(output.contains("Notes"));
assert!(output.contains("declared here"));
}
#[test]
fn test_markdown_renderer_no_emojis() {
let mut renderer = X86MarkdownRenderer::new();
renderer.use_emojis = false;
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("Errors"));
}
#[test]
fn test_markdown_renderer_with_toc() {
let mut renderer = X86MarkdownRenderer::new();
renderer.include_toc = true;
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("Table of Contents"));
}
#[test]
fn test_markdown_renderer_summary_table() {
let renderer = X86MarkdownRenderer::new();
let diags = vec![make_err_diag(), make_warn_diag()];
let output = renderer.render(&diags);
assert!(output.contains("| 1 |"));
assert!(output.contains("| 1 |"));
}
#[test]
fn test_transformer_promote_warnings() {
let mut transformer = X86DiagTransformer::new();
transformer.promote_warnings_to_errors("unused");
let mut diag = make_warn_diag();
transformer.transform(&mut diag);
assert_eq!(diag.severity, DiagSeverity::Error);
}
#[test]
fn test_transformer_demote_errors() {
let mut transformer = X86DiagTransformer::new();
transformer.demote_errors_to_warnings("division");
let mut diag = make_err_diag();
transformer.transform(&mut diag);
assert_eq!(diag.severity, DiagSeverity::Warning);
}
#[test]
fn test_transformer_append_message() {
let mut transformer = X86DiagTransformer::new();
transformer.append_message("division", "[CUSTOM NOTE]");
let mut diag = make_err_diag();
transformer.transform(&mut diag);
assert!(diag.message.contains("[CUSTOM NOTE]"));
}
#[test]
fn test_transformer_strip_fixits() {
let mut transformer = X86DiagTransformer::new();
transformer.strip_fixits("division");
let mut diag = make_err_diag();
diag.fixits.push(FixItHint {
remove_range: SourceRange::point(ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 1,
offset: 0 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
}),
code: "fix".to_string(),
is_insertion: true,
});
transformer.transform(&mut diag);
assert!(diag.fixits.is_empty());
}
#[test]
fn test_transformer_no_match() {
let mut transformer = X86DiagTransformer::new();
transformer.promote_warnings_to_errors("nonexistent pattern");
let mut diag = make_warn_diag();
transformer.transform(&mut diag);
assert_eq!(diag.severity, DiagSeverity::Warning); }
#[test]
fn test_transformer_transform_all() {
let mut transformer = X86DiagTransformer::new();
transformer.promote_warnings_to_errors("unused");
let mut diags = vec![make_err_diag(), make_warn_diag()];
transformer.transform_all(&mut diags);
assert_eq!(diags[0].severity, DiagSeverity::Error); assert_eq!(diags[1].severity, DiagSeverity::Error); }
#[test]
fn test_progress_basic() {
let mut progress = X86DiagnosticProgress::new(100);
assert_eq!(progress.percentage(), 0.0);
progress.advance(&[make_err_diag()]);
assert!(progress.percentage() > 0.0);
assert_eq!(progress.errors, 1);
}
#[test]
fn test_progress_zero_total() {
let progress = X86DiagnosticProgress::new(0);
assert_eq!(progress.percentage(), 100.0);
}
#[test]
fn test_progress_format() {
let mut progress = X86DiagnosticProgress::new(10);
progress.advance(&[make_err_diag(), make_warn_diag()]);
let output = progress.format_progress();
assert!(output.contains("%"));
assert!(output.contains("E:1"));
assert!(output.contains("W:1"));
}
#[test]
fn test_progress_file_tracking() {
let mut progress = X86DiagnosticProgress::new(10);
progress.current_file = Some("test.c".to_string());
progress.file_done();
assert_eq!(progress.files_processed, 1);
}
#[test]
fn test_progress_reset() {
let mut progress = X86DiagnosticProgress::new(10);
progress.advance(&[make_err_diag()]);
progress.reset();
assert_eq!(progress.processed, 0);
assert_eq!(progress.errors, 0);
assert_eq!(progress.files_processed, 0);
}
#[test]
fn test_fixed_renderer_gcc() {
let renderer = X86FixedDiagRenderer::new(X86DiagOutputFormat::Gcc);
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("test.c:2:13:"));
assert!(output.contains("error:"));
}
#[test]
fn test_fixed_renderer_json() {
let renderer = X86FixedDiagRenderer::new(X86DiagOutputFormat::Json);
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("\"severity\""));
}
#[test]
fn test_fixed_renderer_compact() {
let renderer = X86FixedDiagRenderer::new(X86DiagOutputFormat::Compact);
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("[ERROR]"));
}
#[test]
fn test_fixed_renderer_single() {
let renderer = X86FixedDiagRenderer::new(X86DiagOutputFormat::Vi);
let output = renderer.render_single(&make_err_diag());
assert!(output.contains("test.c +2"));
}
#[test]
fn test_metrics_basic() {
let diags = vec![make_err_diag(), make_warn_diag()];
let metrics = X86DiagMetrics::compute(&diags, 100);
assert!(metrics.error_density > 0.0);
assert!(metrics.warning_density > 0.0);
}
#[test]
fn test_metrics_zero_lines() {
let metrics = X86DiagMetrics::compute(&[make_err_diag()], 0);
assert_eq!(metrics.error_density, 0.0);
}
#[test]
fn test_metrics_fixit_coverage() {
let mut diag_with_fixit = make_err_diag();
diag_with_fixit.fixits.push(FixItHint {
remove_range: SourceRange::point(ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 1,
offset: 0 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
}),
code: "fix".to_string(),
is_insertion: true,
});
let metrics = X86DiagMetrics::compute(&[make_err_diag(), diag_with_fixit], 50);
assert_eq!(metrics.fixit_coverage, 50.0);
}
#[test]
fn test_metrics_format_report() {
let metrics = X86DiagMetrics::compute(&[make_err_diag(), make_warn_diag()], 100);
let report = metrics.format_report();
assert!(report.contains("Diagnostic Quality Metrics"));
assert!(report.contains("Error density"));
assert!(report.contains("Fix-it coverage"));
}
#[test]
fn test_metrics_top_category() {
let diags = vec![make_err_diag(), make_warn_diag()];
let metrics = X86DiagMetrics::compute(&diags, 100);
assert_eq!(metrics.top_category, Some("Semantic".to_string()));
}
#[test]
fn test_preset_all_names() {
let presets = X86DiagPreset::all();
assert_eq!(presets.len(), 6);
for preset in &presets {
let name = preset.name();
assert!(!name.is_empty());
}
}
#[test]
fn test_preset_from_name() {
assert!(X86DiagPreset::from_name("verbose").is_some());
assert!(X86DiagPreset::from_name("ide").is_some());
assert!(X86DiagPreset::from_name("ci").is_some());
assert!(X86DiagPreset::from_name("review").is_some());
assert!(X86DiagPreset::from_name("stats").is_some());
assert!(X86DiagPreset::from_name("compact").is_some());
assert!(X86DiagPreset::from_name("unknown").is_none());
}
#[test]
fn test_preset_create_renderer() {
let renderer = X86DiagPreset::VerboseTerminal.create_renderer();
assert!(renderer.options.show_carets);
let renderer = X86DiagPreset::CompactLines.create_renderer();
assert!(!renderer.options.show_carets);
let renderer = X86DiagPreset::IdeIntegration.create_renderer();
assert!(!renderer.options.show_carets);
}
#[test]
fn test_preset_code_review_has_filter() {
let renderer = X86DiagPreset::CodeReview.create_renderer();
let sys_headers = HashSet::new();
let note = StructuredDiagnostic {
id: DiagID::NotePreviousDefinition,
severity: DiagSeverity::Note,
message: "test".to_string(),
location: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 1,
column: 1,
offset: 0 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
ranges: vec![],
notes: vec![],
fixits: vec![],
flag_name: None,
category: "Test".to_string(),
};
assert!(!renderer.filter.should_show(¬e, &sys_headers));
}
#[test]
fn test_yaml_escape_special_chars() {
assert_eq!(yaml_escape("hello"), "hello");
assert_eq!(yaml_escape("line\nbreak"), "line\\nbreak");
assert_eq!(yaml_escape("quote\""), "quote\\\"");
assert_eq!(yaml_escape("tab\there"), "tab\\there");
}
#[test]
fn test_source_chunk_no_lines() {
let scheme = X86ColorScheme::monochrome();
let chunk = X86DiagSourceChunk {
file: "test.c".to_string(),
start_line: 1,
lines: vec![],
diag_line: 1,
diag_column: 1,
highlights: vec![],
};
let output = chunk.render(&scheme, 8);
assert!(output.is_empty() || !output.contains("|"));
}
#[test]
fn test_source_chunk_first_column() {
let scheme = X86ColorScheme::monochrome();
let chunk = X86DiagSourceChunk {
file: "test.c".to_string(),
start_line: 1,
lines: vec![(1, "int x;".to_string())],
diag_line: 1,
diag_column: 1,
highlights: vec![],
};
let output = chunk.render(&scheme, 8);
assert!(output.contains("^"));
}
#[test]
fn test_progress_bar_50_percent() {
let mut progress = X86DiagnosticProgress::new(10);
progress.advance(&[
make_err_diag(),
make_warn_diag(),
make_err_diag(),
make_warn_diag(),
make_err_diag(),
]);
let bar = progress.format_progress();
assert!(bar.contains("50"));
}
#[test]
fn test_progress_bar_100_percent() {
let mut progress = X86DiagnosticProgress::new(2);
progress.advance(&[make_err_diag(), make_warn_diag()]);
let bar = progress.format_progress();
assert!(bar.contains("100"));
}
#[test]
fn test_metrics_multiple_files() {
let mut diag1 = make_err_diag();
diag1.location.file = PathBuf::from("file_a.c");
let mut diag2 = make_warn_diag();
diag2.location.file = PathBuf::from("file_b.c");
let metrics = X86DiagMetrics::compute(&[diag1, diag2], 200);
assert!(metrics.diags_per_file.contains_key("file_a.c"));
assert!(metrics.diags_per_file.contains_key("file_b.c"));
assert_eq!(metrics.diags_per_file.len(), 2);
}
#[test]
fn test_transformer_then_render() {
let mut transformer = X86DiagTransformer::new();
transformer.promote_warnings_to_errors("unused");
let mut diags = vec![make_warn_diag()];
transformer.transform_all(&mut diags);
let renderer = X86GccRenderer::new();
let output = renderer.render_all(&diags);
assert!(output.contains("error:"));
assert!(!output.contains("warning:"));
}
#[test]
fn test_markdown_renderer_default() {
let r = X86MarkdownRenderer::default();
assert!(r.use_emojis);
assert!(!r.include_toc);
}
#[test]
fn test_transformer_default() {
let t = X86DiagTransformer::default();
let mut diag = make_err_diag();
t.transform(&mut diag);
assert_eq!(diag.severity, DiagSeverity::Error);
}
#[test]
fn test_fixit_multiline_default() {
let ml = X86FixItMultiLine::default();
assert_eq!(ml.tab_stop, 8);
assert!(ml.auto_merge);
}
#[test]
fn test_batch_processor_default() {
let bp = X86DiagBatchProcessor::default();
assert_eq!(bp.batch_size, 100);
}
#[test]
fn test_severity_filter_escalate_notes_in_file() {
let mut filter = X86DiagnosticSeverityFilter::new(DiagSeverity::Warning);
filter.escalate_notes_in_files.insert("test.c".to_string());
let mut note_diag = make_err_diag();
note_diag.severity = DiagSeverity::Note;
note_diag.location.file = PathBuf::from("test.c");
let effective = filter.effective_severity(¬e_diag);
assert_eq!(effective, DiagSeverity::Warning);
}
#[test]
fn test_severity_filter_max_warnings() {
let mut filter = X86DiagnosticSeverityFilter::new(DiagSeverity::Warning);
filter.max_warnings = Some(1);
let warn1 = make_warn_diag();
let mut warn2 = make_warn_diag();
warn2.id = DiagID::WFormat;
let result = filter.apply(&[warn1, warn2]);
assert_eq!(result.len(), 1);
}
#[test]
fn test_csv_quote_embedded_quotes() {
let renderer = X86CsvRenderer::new();
let mut diag = make_err_diag();
diag.message = "contains \"quotes\"".to_string();
let output = renderer.render(&[diag]);
assert!(output.contains("\"\"quotes\"\""));
}
#[test]
fn test_csv_newline_in_field() {
let renderer = X86CsvRenderer::new();
let mut diag = make_err_diag();
diag.message = "line1\nline2".to_string();
let output = renderer.render(&[diag]);
assert!(output.contains("\"line1\nline2\""));
}
#[test]
fn test_html_custom_class_prefix() {
let mut renderer = X86HtmlRenderer::new();
renderer.class_prefix = "custom".to_string();
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("custom-error"));
assert!(!output.contains("clang-diag-error"));
}
#[test]
fn test_html_custom_title() {
let mut renderer = X86HtmlRenderer::new();
renderer.title = "Custom Report".to_string();
let output = renderer.render(&[make_err_diag()]);
assert!(output.contains("Custom Report"));
}
#[test]
fn test_yaml_with_ranges() {
let renderer = X86YamlRenderer::new();
let mut diag = make_err_diag();
diag.ranges.push(SourceRange {
begin: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 2,
column: 10,
offset: 20 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
end: ClangSourceLocation {
file: PathBuf::from("test.c"),
line: 2,
column: 15,
offset: 25 as u64,
is_valid: true,
is_macro_expansion: false,
spelling_loc: None,
expansion_loc: None,
},
});
let output = renderer.render(&[diag]);
assert!(output.contains("ranges:"));
}
#[test]
fn test_output_format_variants() {
let formats = [
X86DiagOutputFormat::Terminal,
X86DiagOutputFormat::Json,
X86DiagOutputFormat::Sarif,
X86DiagOutputFormat::Xml,
X86DiagOutputFormat::Gcc,
X86DiagOutputFormat::Msvc,
X86DiagOutputFormat::Vi,
X86DiagOutputFormat::Compact,
X86DiagOutputFormat::Stats,
];
assert_eq!(formats.len(), 9);
}
}