use std::env;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Mutex;
use regex::Regex;
static CPPLINT_INSTALL_STATE: AtomicU8 = AtomicU8::new(0);
static INSTALL_LOCK: Mutex<()> = Mutex::new(());
#[derive(Debug, Clone)]
pub struct CpplintFixerConfig {
pub header_guard_mode: HeaderGuardMode,
pub todo_username: Option<String>,
pub copyright_template: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HeaderGuardMode {
FixName,
PragmaOnce,
Disabled,
}
impl Default for CpplintFixerConfig {
fn default() -> Self {
Self {
header_guard_mode: HeaderGuardMode::FixName,
todo_username: None,
copyright_template: None,
}
}
}
#[derive(Debug, Clone)]
struct CpplintError {
line: usize,
message: String,
category: String,
}
pub struct CpplintFixer {
config: CpplintFixerConfig,
cached_username: Option<String>,
is_objc: bool,
}
impl CpplintFixer {
pub fn new() -> Self {
Self {
config: CpplintFixerConfig::default(),
cached_username: None,
is_objc: false,
}
}
pub fn with_config(config: CpplintFixerConfig) -> Self {
Self {
config,
cached_username: None,
is_objc: false,
}
}
pub fn set_is_objc(&mut self, is_objc: bool) {
self.is_objc = is_objc;
}
fn has_cpplint() -> bool {
Command::new("cpplint")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn try_install_cpplint() -> bool {
let _lock = INSTALL_LOCK.lock().unwrap();
let state = CPPLINT_INSTALL_STATE.load(Ordering::SeqCst);
if state != 0 {
return state == 2;
}
CPPLINT_INSTALL_STATE.store(1, Ordering::SeqCst);
eprintln!("\n📦 cpplint not found, attempting to install...");
let install_commands = Self::detect_install_commands();
for (cmd_name, args) in &install_commands {
if Self::try_run_installer(cmd_name, args) {
CPPLINT_INSTALL_STATE.store(2, Ordering::SeqCst);
return true;
}
}
let hint = crate::python_tool_install_hint("cpplint");
eprintln!(" ❌ Auto-installation failed. Please install manually:");
eprintln!(" {}\n", hint);
CPPLINT_INSTALL_STATE.store(3, Ordering::SeqCst);
false
}
fn detect_install_commands() -> Vec<(&'static str, Vec<&'static str>)> {
let has = |cmd: &str| {
Command::new(cmd)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if has("uv") {
vec![("uv", vec!["tool", "install", "cpplint"])]
} else if has("pipx") {
vec![("pipx", vec!["install", "cpplint"])]
} else {
vec![
("pip", vec!["install", "cpplint", "--upgrade"]),
("pip3", vec!["install", "cpplint", "--upgrade"]),
]
}
}
fn try_run_installer(cmd_name: &str, args: &[&str]) -> bool {
if !Command::new(cmd_name)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return false;
}
eprintln!(" Using {} to install cpplint...", cmd_name);
let mut child = match Command::new(cmd_name)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(e) => {
eprintln!(" ❌ Failed to start {}: {}", cmd_name, e);
return false;
}
};
if let Some(stderr) = child.stderr.take() {
let reader = BufReader::new(stderr);
for line in reader.lines().map_while(Result::ok) {
if line.contains("Collecting")
|| line.contains("Downloading")
|| line.contains("Installing")
|| line.contains("Successfully")
{
eprintln!(" {}", line);
}
}
}
match child.wait() {
Ok(status) if status.success() => {
if Self::has_cpplint() {
eprintln!(" ✓ cpplint installed successfully!\n");
return true;
}
eprintln!(" ⚠️ Installation completed but cpplint not found in PATH");
eprintln!(" You may need to restart your terminal or add Python's bin directory to PATH\n");
}
Ok(status) => {
eprintln!(" ❌ Installation failed with exit code: {}", status);
}
Err(e) => {
eprintln!(" ❌ Failed to wait for {}: {}", cmd_name, e);
}
}
false
}
fn run_cpplint(path: &Path, is_objc: bool) -> Vec<CpplintError> {
if !Self::has_cpplint() {
let state = CPPLINT_INSTALL_STATE.load(Ordering::SeqCst);
match state {
0 => {
if Self::try_install_cpplint() {
} else {
return Vec::new();
}
}
1 => {
return Vec::new();
}
2 => {
return Vec::new();
}
3 => {
return Vec::new();
}
_ => {
return Vec::new();
}
}
}
let mut cmd = Command::new("cpplint");
if is_objc {
cmd.arg("--extensions=m,mm,h");
cmd.arg("--linelength=150");
} else {
cmd.arg("--linelength=120");
}
cmd.arg(path);
let output = cmd.output();
let output = match output {
Ok(o) => o,
Err(e) => {
eprintln!("[cpplint-fixer] Failed to run cpplint: {}", e);
return Vec::new();
}
};
let stderr = String::from_utf8_lossy(&output.stderr);
let errors = Self::parse_cpplint_output(&stderr);
if std::env::var("LINTHIS_DEBUG").is_ok() {
eprintln!(
"[cpplint-fixer] {} cpplint stderr:\n{}",
path.display(),
stderr
);
eprintln!("[cpplint-fixer] Parsed {} errors", errors.len());
for e in &errors {
eprintln!(
"[cpplint-fixer] line {}: {} [{}]",
e.line, e.message, e.category
);
}
}
errors
}
fn parse_cpplint_output(output: &str) -> Vec<CpplintError> {
let mut errors = Vec::new();
let re = Regex::new(r"^([^:]+):(\d+):\s*(.+)\s+\[([^\]]+)\]\s*\[\d+\]\s*$").unwrap();
for line in output.lines() {
if let Some(caps) = re.captures(line) {
let file_path = &caps[1];
if file_path.starts_with("/Library/Developer/")
|| file_path.starts_with("/System/Library/")
|| file_path.starts_with("/usr/include/")
|| file_path.starts_with("/usr/local/include/")
|| file_path.contains("/SDKs/")
|| file_path.contains(".framework/")
{
continue;
}
if let Ok(line_num) = caps[2].parse::<usize>() {
errors.push(CpplintError {
line: line_num,
message: caps[3].to_string(),
category: caps[4].to_string(),
});
}
}
}
errors
}
fn get_username(&mut self) -> String {
if let Some(ref username) = self.cached_username {
return username.clone();
}
if let Some(ref username) = self.config.todo_username {
self.cached_username = Some(username.clone());
return username.clone();
}
if let Ok(output) = Command::new("git").args(["config", "user.name"]).output() {
if output.status.success() {
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !name.is_empty() {
let username = name.to_lowercase().replace(' ', "_");
self.cached_username = Some(username.clone());
return username;
}
}
}
if let Ok(user) = env::var("USER") {
self.cached_username = Some(user.clone());
return user;
}
"unknown".to_string()
}
pub fn fix_file(&mut self, path: &Path) -> Result<bool, String> {
if !path.exists() {
return Err(format!("File not found: {}", path.display()));
}
let debug = std::env::var("LINTHIS_DEBUG").is_ok();
let errors = Self::run_cpplint(path, self.is_objc);
if errors.is_empty() {
if debug {
eprintln!("[cpplint-fixer] No errors found for {}", path.display());
}
return Ok(false);
}
if debug {
eprintln!(
"[cpplint-fixer] Processing {} errors for {}",
errors.len(),
path.display()
);
}
let content =
fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
let mut modified = false;
for error in &errors {
if self.fix_single_error(&mut lines, error, debug) {
modified = true;
}
}
if modified {
if debug {
eprintln!("[cpplint-fixer] Lines before write:");
for (i, line) in lines.iter().enumerate() {
eprintln!("[cpplint-fixer] [{}] {:?}", i, line);
}
}
let new_content = lines.join("\n") + if content.ends_with('\n') { "\n" } else { "" };
fs::write(path, new_content).map_err(|e| format!("Failed to write file: {}", e))?;
}
Ok(modified)
}
fn is_objc_incompatible(category: &str) -> bool {
matches!(
category,
"build/header_guard" | "readability/casting" | "whitespace/operators"
)
}
fn dispatch_fix(&mut self, lines: &mut Vec<String>, error: &CpplintError) -> Option<bool> {
Some(match error.category.as_str() {
"build/header_guard" => self.fix_header_guard_by_mode(lines, error),
"readability/todo" => self.fix_todo_from_error(lines, error),
"legal/copyright" => self.fix_copyright_from_error(lines),
"readability/casting" => self.fix_c_style_cast(lines, error),
"readability/check" => self.fix_assert_check(lines, error),
"whitespace/comments" => self.fix_comment_spacing(lines, error),
"whitespace/semicolon" => self.fix_empty_semicolon(lines, error),
"whitespace/comma" => self.fix_comma_spacing(lines, error),
"whitespace/operators" => self.fix_operator_spacing(lines, error),
_ => return None,
})
}
fn fix_single_error(
&mut self,
lines: &mut Vec<String>,
error: &CpplintError,
debug: bool,
) -> bool {
if self.is_objc && Self::is_objc_incompatible(&error.category) {
if debug {
eprintln!("[cpplint-fixer] Skipping {} for OC file", error.category);
}
return false;
}
let fixed = match self.dispatch_fix(lines, error) {
Some(result) => result,
None => {
if debug {
eprintln!(
"[cpplint-fixer] Skipping unsupported category: {}",
error.category
);
}
return false;
}
};
if fixed && debug {
eprintln!(
"[cpplint-fixer] Fixed {} at line {}",
error.category, error.line
);
}
fixed
}
fn fix_header_guard_by_mode(&mut self, lines: &mut Vec<String>, error: &CpplintError) -> bool {
match self.config.header_guard_mode {
HeaderGuardMode::FixName => self.fix_header_guard_from_error(lines, error),
HeaderGuardMode::PragmaOnce => self.convert_to_pragma_once(lines),
HeaderGuardMode::Disabled => false,
}
}
fn fix_header_guard_from_error(&self, lines: &mut Vec<String>, error: &CpplintError) -> bool {
let debug = std::env::var("LINTHIS_DEBUG").is_ok();
if debug {
eprintln!(
"[cpplint-fixer] fix_header_guard_from_error: line={}, msg={}",
error.line, error.message
);
}
let suggested_guard = match Self::extract_guard_name(&error.message) {
Some(g) => g,
None => return false,
};
if error.line == 0 || error.message.contains("No #ifndef header guard found") {
return self.insert_header_guard(lines, &suggested_guard);
}
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
if line.trim().starts_with("#ifndef") {
lines[line_idx] = format!("#ifndef {}", suggested_guard);
if line_idx + 1 < lines.len() && lines[line_idx + 1].trim().starts_with("#define") {
lines[line_idx + 1] = format!("#define {}", suggested_guard);
}
return true;
}
if line.trim().starts_with("#endif") {
lines[line_idx] = format!("#endif // {}", suggested_guard);
return true;
}
false
}
fn extract_guard_name(message: &str) -> Option<String> {
if message.contains("please use:") {
message
.split("please use:")
.nth(1)
.map(|s| s.trim().to_string())
} else if message.contains("#endif line should be") {
Regex::new(r#"#endif\s+//\s+(\w+)"#)
.ok()
.and_then(|re| re.captures(message))
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
} else if message.contains("suggested CPP variable is:") {
message
.split("suggested CPP variable is:")
.nth(1)
.map(|s| s.trim().to_string())
} else {
None
}
}
fn insert_header_guard(&self, lines: &mut Vec<String>, guard_name: &str) -> bool {
let mut insert_idx = 0;
let mut in_block_comment = false;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("/*") {
in_block_comment = true;
}
if in_block_comment {
if trimmed.contains("*/") {
in_block_comment = false;
}
insert_idx = i + 1;
continue;
}
if trimmed.starts_with("//") {
insert_idx = i + 1;
continue;
}
if trimmed.is_empty() && insert_idx > 0 {
insert_idx = i + 1;
continue;
}
break;
}
if lines.iter().any(|l| l.trim().starts_with("#ifndef")) {
return false;
}
if insert_idx > 0 && !lines[insert_idx - 1].trim().is_empty() {
lines.insert(insert_idx, String::new());
insert_idx += 1;
}
lines.insert(insert_idx, format!("#ifndef {}", guard_name));
lines.insert(insert_idx + 1, format!("#define {}", guard_name));
lines.insert(insert_idx + 2, String::new());
if !lines.last().is_none_or(|l| l.trim().is_empty()) {
lines.push(String::new());
}
lines.push(format!("#endif // {}", guard_name));
true
}
fn convert_to_pragma_once(&self, lines: &mut Vec<String>) -> bool {
let mut ifndef_idx: Option<usize> = None;
let mut define_idx: Option<usize> = None;
let mut endif_idx: Option<usize> = None;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if ifndef_idx.is_none() && trimmed.starts_with("#ifndef") {
ifndef_idx = Some(i);
} else if ifndef_idx.is_some() && define_idx.is_none() && trimmed.starts_with("#define")
{
define_idx = Some(i);
} else if trimmed.starts_with("#endif") {
endif_idx = Some(i);
}
}
let (ifndef_idx, define_idx, endif_idx) = match (ifndef_idx, define_idx, endif_idx) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => return false,
};
if define_idx != ifndef_idx + 1 {
return false;
}
if lines.iter().any(|l| l.trim() == "#pragma once") {
return false;
}
lines[ifndef_idx] = "#pragma once".to_string();
lines[define_idx] = String::new();
lines[endif_idx] = String::new();
lines.retain(|l| !l.is_empty() || l.trim() != "");
true
}
fn fix_todo_from_error(&mut self, lines: &mut [String], error: &CpplintError) -> bool {
if !error.message.contains("Missing username in TODO") {
return false;
}
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
let username = self.get_username();
if let Some(todo_pos) = line.find("TODO") {
let prefix = &line[..todo_pos];
let after_todo = &line[todo_pos + 4..];
if after_todo.trim_start().starts_with('(') {
return false;
}
let rest = after_todo.trim_start_matches([':', ' ']).trim();
lines[line_idx] = if rest.is_empty() {
format!("{}TODO({}): ", prefix, username)
} else {
format!("{}TODO({}): {}", prefix, username, rest)
};
return true;
}
false
}
fn fix_copyright_from_error(&self, lines: &mut Vec<String>) -> bool {
let first_lines: String = lines
.iter()
.take(10)
.cloned()
.collect::<Vec<_>>()
.join("\n");
if first_lines.to_lowercase().contains("copyright") {
return false;
}
let template = match &self.config.copyright_template {
Some(t) => t.clone(),
None => return false,
};
let year = chrono::Utc::now().format("%Y").to_string();
let copyright = template.replace("{year}", &year);
let copyright_lines: Vec<String> = copyright.lines().map(|s| s.to_string()).collect();
for (i, cline) in copyright_lines.into_iter().enumerate() {
lines.insert(i, cline);
}
lines.insert(copyright.lines().count(), String::new());
true
}
fn fix_c_style_cast(&self, lines: &mut [String], error: &CpplintError) -> bool {
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
let nullptr_re = Regex::new(r"\(\(void\s*\*\)\s*0\)|\(void\s*\*\)\s*0").ok();
if let Some(re) = nullptr_re {
if re.is_match(line) {
lines[line_idx] = re.replace_all(line, "nullptr").to_string();
return true;
}
}
let cast_re = Regex::new(r"\((\w+\s*\*+)\)\s*(\w+)").ok();
if let Some(re) = cast_re {
if let Some(caps) = re.captures(line) {
let cast_type = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let expr = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if !cast_type.is_empty() && !expr.is_empty() {
let replacement = format!("reinterpret_cast<{}>({})", cast_type, expr);
lines[line_idx] = re.replace(line, replacement.as_str()).to_string();
return true;
}
}
}
false
}
fn fix_comment_spacing(&self, lines: &mut [String], error: &CpplintError) -> bool {
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
let fixed = Self::fix_comment_spacing_line(line);
if fixed != *line {
lines[line_idx] = fixed;
true
} else {
false
}
}
fn fix_comment_spacing_line(line: &str) -> String {
let Some(comment_pos) = Self::find_real_comment_pos(line) else {
return line.to_string();
};
let before_comment = &line[..comment_pos];
let comment_part = &line[comment_pos..];
let slash_count = comment_part.chars().take_while(|&c| c == '/').count();
let after_slashes = &comment_part[slash_count..];
if !after_slashes.is_empty() {
let first_char = after_slashes.chars().next().unwrap();
if first_char != ' ' && first_char != '\n' && first_char != '\r' {
return format!(
"{}{} {}",
before_comment,
"/".repeat(slash_count),
after_slashes
);
}
}
line.to_string()
}
fn find_real_comment_pos(line: &str) -> Option<usize> {
let mut search_start = 0;
loop {
let rest = &line[search_start..];
let rel_pos = rest.find("//")?;
let abs_pos = search_start + rel_pos;
let before_comment = &line[..abs_pos];
if !Self::is_in_cpp_string(before_comment) {
return Some(abs_pos);
}
search_start = abs_pos + 2;
}
}
fn is_in_cpp_string(line: &str) -> bool {
let line = line.replace("\\\\", "XX");
let total_quotes = line.matches('"').count();
let escaped_quotes = line.matches("\\\"").count();
let char_literal_quotes = line.matches("'\"'").count();
let effective_quotes = total_quotes - escaped_quotes - char_literal_quotes;
(effective_quotes & 1) == 1
}
fn fix_assert_check(&self, lines: &mut [String], error: &CpplintError) -> bool {
if !error.message.contains("Consider using ASSERT_") {
return false;
}
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let replacements = [
(r"ASSERT_TRUE\s*\(\s*(.+?)\s*==\s*(.+?)\s*\)", "ASSERT_EQ"),
(r"ASSERT_TRUE\s*\(\s*(.+?)\s*!=\s*(.+?)\s*\)", "ASSERT_NE"),
(r"ASSERT_FALSE\s*\(\s*(.+?)\s*==\s*(.+?)\s*\)", "ASSERT_NE"),
];
for (pattern, macro_name) in &replacements {
if Self::try_replace_assert(&mut lines[line_idx], pattern, macro_name) {
return true;
}
}
false
}
fn try_replace_assert(line: &mut String, pattern: &str, macro_name: &str) -> bool {
let re = match Regex::new(pattern) {
Ok(r) => r,
Err(_) => return false,
};
let Some(caps) = re.captures(line) else {
return false;
};
let lhs = caps.get(1).map(|m| m.as_str().trim()).unwrap_or("");
let rhs = caps.get(2).map(|m| m.as_str().trim()).unwrap_or("");
if lhs.is_empty() || rhs.is_empty() {
return false;
}
let replacement = format!("{}({}, {})", macro_name, lhs, rhs);
*line = re.replace(line.as_str(), replacement.as_str()).to_string();
true
}
fn fix_empty_semicolon(&self, lines: &mut [String], error: &CpplintError) -> bool {
if !error.message.contains("Line contains only semicolon") {
return false;
}
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
if let Ok(re) = Regex::new(r"^(\s*);(\s*(?://.*)?)?$") {
if let Some(caps) = re.captures(line) {
let indent = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let suffix = caps.get(2).map(|m| m.as_str()).unwrap_or("");
lines[line_idx] = format!("{}{}{}", indent, "{}", suffix);
return true;
}
}
false
}
fn fix_comma_spacing(&self, lines: &mut [String], error: &CpplintError) -> bool {
if !error.message.contains("Missing space after ,") {
return false;
}
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
if line.trim_start().starts_with("#pragma mark") {
return false;
}
let mut result = String::with_capacity(line.len() + 10);
let mut chars = line.chars().peekable();
let mut modified = false;
while let Some(c) = chars.next() {
result.push(c);
if c == ',' {
if let Some(&next) = chars.peek() {
if next != ' ' && next != '\n' && next != '\r' {
result.push(' ');
modified = true;
}
}
}
}
if modified {
lines[line_idx] = result;
true
} else {
false
}
}
fn fix_operator_spacing(&self, lines: &mut [String], error: &CpplintError) -> bool {
if !error.message.contains("Missing spaces around =") {
return false;
}
let line_idx = error.line.saturating_sub(1);
if line_idx >= lines.len() {
return false;
}
let line = &lines[line_idx];
if let Ok(re) = Regex::new(r"([^\s=!<>+\-*/%&|^])=([^=\s])") {
let result = re.replace_all(line, "$1 = $2").to_string();
if result != *line {
lines[line_idx] = result;
return true;
}
}
false
}
}
impl Default for CpplintFixer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cpplint_output() {
let output = r##"test.h:8: #ifndef header guard has wrong style, please use: FOO_BAR_H_ [build/header_guard] [5]
test.h:76: #endif line should be "#endif // FOO_BAR_H_" [build/header_guard] [5]
test.cc:17: Missing username in TODO; it should look like "// TODO(my_username): Stuff." [readability/todo] [2]
"##;
let errors = CpplintFixer::parse_cpplint_output(output);
assert_eq!(errors.len(), 3);
assert_eq!(errors[0].line, 8);
assert_eq!(errors[0].category, "build/header_guard");
assert!(errors[0].message.contains("please use: FOO_BAR_H_"));
assert_eq!(errors[1].line, 76);
assert_eq!(errors[1].category, "build/header_guard");
assert_eq!(errors[2].line, 17);
assert_eq!(errors[2].category, "readability/todo");
}
#[test]
fn test_parse_missing_header_guard() {
let output = r##"test.h:0: No #ifndef header guard found, suggested CPP variable is: TEST_H_ [build/header_guard] [5]
"##;
let errors = CpplintFixer::parse_cpplint_output(output);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 0);
assert_eq!(errors[0].category, "build/header_guard");
assert!(errors[0]
.message
.contains("suggested CPP variable is: TEST_H_"));
}
#[test]
fn test_insert_missing_header_guard() {
let fixer = CpplintFixer::new();
let mut lines = vec![
"// Copyright notice".to_string(),
"".to_string(),
"#include <stdio.h>".to_string(),
"".to_string(),
"void foo();".to_string(),
];
let error = CpplintError {
line: 0,
message: "No #ifndef header guard found, suggested CPP variable is: TEST_H_"
.to_string(),
category: "build/header_guard".to_string(),
};
assert!(fixer.fix_header_guard_from_error(&mut lines, &error));
assert!(lines.iter().any(|l| l.contains("#ifndef TEST_H_")));
assert!(lines.iter().any(|l| l.contains("#define TEST_H_")));
assert!(lines.iter().any(|l| l.contains("#endif // TEST_H_")));
}
#[test]
fn test_fix_header_guard_from_error() {
let fixer = CpplintFixer::new();
let mut lines = vec![
"#ifndef OLD_GUARD".to_string(),
"#define OLD_GUARD".to_string(),
"// content".to_string(),
"#endif".to_string(),
];
let error = CpplintError {
line: 1,
message: "#ifndef header guard has wrong style, please use: NEW_GUARD_H_".to_string(),
category: "build/header_guard".to_string(),
};
assert!(fixer.fix_header_guard_from_error(&mut lines, &error));
assert_eq!(lines[0], "#ifndef NEW_GUARD_H_");
assert_eq!(lines[1], "#define NEW_GUARD_H_");
}
#[test]
fn test_fix_todo_from_error() {
let mut fixer = CpplintFixer::new();
fixer.cached_username = Some("testuser".to_string());
let mut lines = vec![
"// TODO: fix this".to_string(),
"// TODO(existing): keep this".to_string(),
];
let error = CpplintError {
line: 1,
message:
r#"Missing username in TODO; it should look like "// TODO(my_username): Stuff.""#
.to_string(),
category: "readability/todo".to_string(),
};
assert!(fixer.fix_todo_from_error(&mut lines, &error));
assert_eq!(lines[0], "// TODO(testuser): fix this");
assert_eq!(lines[1], "// TODO(existing): keep this");
}
#[test]
fn test_fix_endif_line() {
let fixer = CpplintFixer::new();
let mut lines = vec![
"#ifndef GUARD_H_".to_string(),
"#define GUARD_H_".to_string(),
"// content".to_string(),
"#endif".to_string(),
];
let error = CpplintError {
line: 4,
message: r##"#endif line should be "#endif // GUARD_H_""##.to_string(),
category: "build/header_guard".to_string(),
};
assert!(fixer.fix_header_guard_from_error(&mut lines, &error));
assert_eq!(lines[3], "#endif // GUARD_H_");
}
#[test]
fn test_fix_comment_spacing_cpplint() {
let fixer = CpplintFixer::new();
let mut lines = vec![
"int x = 1; //comment".to_string(),
"int y = 2; // already spaced".to_string(),
];
let error = CpplintError {
line: 1,
message: "Should have a space between // and comment".to_string(),
category: "whitespace/comments".to_string(),
};
assert!(fixer.fix_comment_spacing(&mut lines, &error));
assert_eq!(lines[0], "int x = 1; // comment");
assert_eq!(lines[1], "int y = 2; // already spaced");
}
#[test]
fn test_fix_comment_spacing_triple_slash() {
let fixer = CpplintFixer::new();
let mut lines = vec!["///doc comment".to_string()];
let error = CpplintError {
line: 1,
message: "Should have a space between // and comment".to_string(),
category: "whitespace/comments".to_string(),
};
assert!(fixer.fix_comment_spacing(&mut lines, &error));
assert_eq!(lines[0], "/// doc comment");
}
#[test]
fn test_fix_comment_spacing_preserves_url() {
let fixer = CpplintFixer::new();
let mut lines = vec!["return @\"https://example.com\";".to_string()];
let error = CpplintError {
line: 1,
message: "Should have a space between // and comment".to_string(),
category: "whitespace/comments".to_string(),
};
assert!(!fixer.fix_comment_spacing(&mut lines, &error));
assert_eq!(lines[0], "return @\"https://example.com\";");
}
#[test]
fn test_fix_comment_spacing_url_and_comment() {
let fixer = CpplintFixer::new();
let mut lines = vec!["NSString *url = @\"https://example.com\"; //comment".to_string()];
let error = CpplintError {
line: 1,
message: "Should have a space between // and comment".to_string(),
category: "whitespace/comments".to_string(),
};
assert!(fixer.fix_comment_spacing(&mut lines, &error));
assert_eq!(
lines[0],
"NSString *url = @\"https://example.com\"; // comment"
);
}
#[test]
fn test_insert_header_guard_after_block_comment() {
let fixer = CpplintFixer::new();
let mut lines = vec![
"/*".to_string(),
" * Copyright 2024".to_string(),
" */".to_string(),
"".to_string(),
"#include <stdio.h>".to_string(),
];
let error = CpplintError {
line: 0,
message: "No #ifndef header guard found, suggested CPP variable is: TEST_H_"
.to_string(),
category: "build/header_guard".to_string(),
};
assert!(fixer.fix_header_guard_from_error(&mut lines, &error));
let ifndef_pos = lines
.iter()
.position(|l| l.contains("#ifndef TEST_H_"))
.unwrap();
let block_end_pos = lines.iter().position(|l| l.contains("*/")).unwrap();
assert!(ifndef_pos > block_end_pos);
}
#[test]
fn test_parse_whitespace_comments_error() {
let output = r##"test.cc:5: Should have a space between // and comment [whitespace/comments] [4]
"##;
let errors = CpplintFixer::parse_cpplint_output(output);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 5);
assert_eq!(errors[0].category, "whitespace/comments");
}
}