use crate::fixers::cpplint::{CpplintFixer, CpplintFixerConfig, HeaderGuardMode};
use crate::fixers::source::SourceFixer;
use crate::formatters::Formatter;
use crate::utils::types::FormatResult;
use crate::{Language, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
pub struct CppFormatter {
use_clang_tidy_fix: bool,
use_cpplint_fix: bool,
compile_commands_dir: Option<PathBuf>,
cpplint_fixer: Mutex<CpplintFixer>,
}
impl CppFormatter {
pub fn new() -> Self {
Self {
use_clang_tidy_fix: true, use_cpplint_fix: true, compile_commands_dir: None,
cpplint_fixer: Mutex::new(CpplintFixer::new()),
}
}
pub fn with_clang_tidy_fix(mut self, enable: bool) -> Self {
self.use_clang_tidy_fix = enable;
self
}
pub fn with_cpplint_fix(mut self, enable: bool) -> Self {
self.use_cpplint_fix = enable;
self
}
pub fn with_compile_commands_dir(mut self, path: PathBuf) -> Self {
self.compile_commands_dir = Some(path);
self
}
pub fn with_cpplint_config(self, config: CpplintFixerConfig) -> Self {
*self.cpplint_fixer.lock().unwrap() = CpplintFixer::with_config(config);
self
}
pub fn with_header_guard_mode(self, mode: HeaderGuardMode) -> Self {
{
let mut fixer = self.cpplint_fixer.lock().unwrap();
let config = CpplintFixerConfig {
header_guard_mode: mode,
..Default::default()
};
*fixer = CpplintFixer::with_config(config);
}
self
}
fn has_clang_tidy() -> bool {
Command::new("clang-tidy")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn find_clang_tidy_config(start_path: &Path) -> Option<PathBuf> {
let mut current = if start_path.is_file() {
start_path.parent()?.to_path_buf()
} else {
start_path.to_path_buf()
};
loop {
let config_path = current.join(".clang-tidy");
if config_path.exists() {
return Some(config_path);
}
if !current.pop() {
break;
}
}
None
}
fn find_clang_format_config(start_path: &Path, language: &str) -> Option<PathBuf> {
let mut current = if start_path.is_file() {
start_path.parent()?.to_path_buf()
} else {
start_path.to_path_buf()
};
let mut search_dir = current.clone();
loop {
let linthis_config = search_dir
.join(".linthis")
.join("configs")
.join(language)
.join(".clang-format");
if linthis_config.exists() {
return Some(linthis_config);
}
if !search_dir.pop() {
break;
}
}
loop {
let config_path = current.join(".clang-format");
if config_path.exists() {
return Some(config_path);
}
if !current.pop() {
break;
}
}
None
}
fn find_compile_commands(start_path: &Path) -> Option<PathBuf> {
let mut current = if start_path.is_file() {
start_path.parent()?.to_path_buf()
} else {
start_path.to_path_buf()
};
loop {
if current.join("compile_commands.json").exists() {
return Some(current.clone());
}
for build_dir in &[
"build",
"Build",
"out",
"cmake-build-debug",
"cmake-build-release",
] {
let compile_db = current.join(build_dir).join("compile_commands.json");
if compile_db.exists() {
return Some(current.join(build_dir));
}
}
if let Some(found) = Self::find_compile_commands_recursive(¤t, 0, 6) {
return Some(found);
}
if !current.pop() {
break;
}
}
None
}
fn find_compile_commands_recursive(
dir: &Path,
depth: usize,
max_depth: usize,
) -> Option<PathBuf> {
if depth >= max_depth {
return None;
}
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = path.file_name().and_then(|n| n.to_str())?;
let name_lower = name.to_lowercase();
if is_build_directory(&name_lower, depth) {
if path.join("compile_commands.json").exists() {
return Some(path);
}
if let Some(found) =
Self::find_compile_commands_recursive(&path, depth + 1, max_depth)
{
return Some(found);
}
}
}
None
}
fn run_clang_tidy_fix(&self, path: &Path) -> Result<bool> {
if !Self::has_clang_tidy() {
return Ok(false);
}
if std::env::var("LINTHIS_SKIP_CLANG_TIDY").is_ok() {
return Ok(false);
}
let build_path = if let Some(ref build_path) = self.compile_commands_dir {
Some(build_path.clone())
} else {
Self::find_compile_commands(path)
};
if build_path.is_none() {
return Ok(false);
}
let mut cmd = Command::new("clang-tidy");
cmd.arg(path);
cmd.arg("--fix");
if let Some(config) = Self::find_clang_tidy_config(path) {
cmd.arg(format!("--config-file={}", config.display()));
}
cmd.arg(format!("-p={}", build_path.unwrap().display()));
let output = cmd.output().map_err(|e| {
crate::LintisError::formatter("clang-tidy", path, format!("Failed to run --fix: {}", e))
})?;
Ok(output.status.success() || !output.stdout.is_empty())
}
fn run_pre_format_fixers(&self, path: &Path, language: &str) {
if self.use_cpplint_fix {
if let Ok(mut fixer) = self.cpplint_fixer.lock() {
fixer.set_is_objc(language == "oc");
let _ = fixer.fix_file(path);
}
}
if self.use_clang_tidy_fix && language != "oc" {
let _ = self.run_clang_tidy_fix(path);
}
}
fn run_clang_format(&self, path: &Path, language: &str) -> Result<Option<FormatResult>> {
let mut cmd = Command::new("clang-format");
cmd.arg("-i");
if let Some(config_path) = Self::find_clang_format_config(path, language) {
cmd.arg(format!("-style=file:{}", config_path.display()));
} else {
cmd.arg("-style=Google");
}
cmd.arg(path);
let output = cmd.output().map_err(|e| {
crate::LintisError::formatter("clang-format", path, format!("Failed to run: {}", e))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Ok(Some(FormatResult::error(
path.to_path_buf(),
format!("clang-format failed: {}", stderr),
)));
}
Ok(None)
}
fn run_post_format_fixers(path: &Path, language: &str) -> Result<()> {
SourceFixer::fix_comment_spacing(path)?;
SourceFixer::fix_todo_comments(path)?;
SourceFixer::fix_lone_semicolon(path)?;
let max_line_length = if language == "oc" { 150 } else { 120 };
SourceFixer::fix_long_comments(path, max_line_length)?;
if language == "oc" {
SourceFixer::fix_pragma_separators(path)?;
}
Ok(())
}
}
fn is_build_directory(name_lower: &str, depth: usize) -> bool {
if name_lower.starts_with("cmake")
|| name_lower.starts_with("build")
|| name_lower.starts_with("out")
|| name_lower.ends_with("-build")
|| name_lower.ends_with("_build")
{
return true;
}
if depth > 0 {
return is_platform_subdirectory(name_lower);
}
false
}
fn is_platform_subdirectory(name_lower: &str) -> bool {
const PLATFORM_KEYWORDS: &[&str] = &[
"android", "ios", "linux", "windows", "arm", "x86", "static", "shared", "debug", "release",
];
PLATFORM_KEYWORDS.iter().any(|kw| name_lower.contains(kw))
}
impl Default for CppFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for CppFormatter {
fn name(&self) -> &str {
match (
self.use_clang_tidy_fix && Self::has_clang_tidy(),
self.use_cpplint_fix,
) {
(true, true) => "clang-format + clang-tidy + cpplint-fix",
(true, false) => "clang-format + clang-tidy",
(false, true) => "clang-format + cpplint-fix",
(false, false) => "clang-format",
}
}
fn supported_languages(&self) -> &[Language] {
&[Language::Cpp, Language::ObjectiveC]
}
fn format(&self, path: &Path) -> Result<FormatResult> {
let language = Self::detect_language(path);
let original = fs::read_to_string(path).map_err(|e| {
crate::LintisError::formatter(
"clang-format",
path,
format!("Failed to read file: {}", e),
)
})?;
self.run_pre_format_fixers(path, language);
let format_result = self.run_clang_format(path, language)?;
if let Some(err_result) = format_result {
return Ok(err_result);
}
Self::run_post_format_fixers(path, language)?;
let new_content = fs::read_to_string(path).map_err(|e| {
crate::LintisError::formatter(
"clang-format",
path,
format!("Failed to read formatted file: {}", e),
)
})?;
if original == new_content {
Ok(FormatResult::unchanged(path.to_path_buf()))
} else {
Ok(FormatResult::changed(path.to_path_buf()))
}
}
fn check(&self, path: &Path) -> Result<bool> {
let language = Self::detect_language(path);
let current = fs::read_to_string(path).map_err(|e| {
crate::LintisError::formatter(
"clang-format",
path,
format!("Failed to read file: {}", e),
)
})?;
let mut cmd = Command::new("clang-format");
if let Some(config_path) = Self::find_clang_format_config(path, language) {
cmd.arg(format!("-style=file:{}", config_path.display()));
} else {
cmd.arg("-style=Google");
}
let output = cmd.arg(path).output().map_err(|e| {
crate::LintisError::formatter("clang-format", path, format!("Failed to run: {}", e))
})?;
let formatted = String::from_utf8_lossy(&output.stdout);
Ok(current != formatted.as_ref())
}
fn is_available(&self) -> bool {
Command::new("clang-format")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
impl CppFormatter {
fn detect_language(path: &Path) -> &'static str {
let debug = std::env::var("LINTHIS_DEBUG").is_ok();
match path.extension().and_then(|e| e.to_str()) {
Some("m") | Some("mm") | Some("M") | Some("MM") => {
if debug {
eprintln!(
"[cpp-formatter] {} detected as OC (by extension)",
path.display()
);
}
"oc"
}
Some("h") | Some("H") => {
if Self::contains_objc_syntax(path) {
if debug {
eprintln!(
"[cpp-formatter] {} detected as OC (by content)",
path.display()
);
}
"oc"
} else {
if debug {
eprintln!(
"[cpp-formatter] {} detected as C++ (no OC syntax found)",
path.display()
);
}
"cpp"
}
}
_ => {
if debug {
eprintln!(
"[cpp-formatter] {} detected as C++ (by extension)",
path.display()
);
}
"cpp"
}
}
}
fn contains_objc_syntax(path: &Path) -> bool {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
};
let oc_patterns = [
"@import", "@interface",
"@implementation",
"@protocol",
"@property",
"@synthesize",
"@dynamic",
"@selector",
"@class",
"@end",
"NS_ASSUME_NONNULL_BEGIN",
"NS_ENUM",
"NS_OPTIONS",
"nullable",
"nonnull",
"+ (", "- (", " @\"", " @[", ];
for pattern in oc_patterns {
if content.contains(pattern) {
return true;
}
}
if Self::contains_ns_type(&content) {
return true;
}
false
}
fn contains_ns_type(content: &str) -> bool {
let bytes = content.as_bytes();
let len = bytes.len();
for i in 0..len.saturating_sub(2) {
if bytes[i] == b'N' && bytes[i + 1] == b'S' {
let next_char = bytes[i + 2];
if next_char.is_ascii_uppercase() {
if i == 0 || !is_identifier_char(bytes[i - 1]) {
return true;
}
}
}
}
false
}
}
fn is_identifier_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_header(content: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new().suffix(".h").tempfile().unwrap();
file.write_all(content.as_bytes()).unwrap();
file
}
#[test]
fn test_detect_language_m_file() {
let path = std::path::Path::new("test.m");
assert_eq!(CppFormatter::detect_language(path), "oc");
}
#[test]
fn test_detect_language_mm_file() {
let path = std::path::Path::new("test.mm");
assert_eq!(CppFormatter::detect_language(path), "oc");
}
#[test]
fn test_detect_language_cpp_file() {
let path = std::path::Path::new("test.cpp");
assert_eq!(CppFormatter::detect_language(path), "cpp");
}
#[test]
fn test_detect_language_h_file_cpp() {
let file = create_temp_header("#include <iostream>\nvoid foo();\n");
assert_eq!(CppFormatter::detect_language(file.path()), "cpp");
}
#[test]
fn test_detect_language_h_file_oc_interface() {
let file = create_temp_header("@interface MyClass : NSObject\n@end\n");
assert_eq!(CppFormatter::detect_language(file.path()), "oc");
}
#[test]
fn test_detect_language_h_file_oc_property() {
let file = create_temp_header("@property (nonatomic) NSString *name;\n");
assert_eq!(CppFormatter::detect_language(file.path()), "oc");
}
#[test]
fn test_contains_objc_syntax_interface() {
let file = create_temp_header("@interface Test\n@end\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_implementation() {
let file = create_temp_header("@implementation Test\n@end\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_protocol() {
let file = create_temp_header("@protocol MyProtocol\n@end\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_ns_enum() {
let file = create_temp_header("typedef NS_ENUM(NSUInteger, MyEnum) {\n};\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_ns_options() {
let file = create_temp_header("typedef NS_OPTIONS(NSUInteger, MyOptions) {\n};\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsinteger() {
let file = create_temp_header("- (NSInteger)count;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsuinteger() {
let file = create_temp_header("NSUInteger value = 0;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsstring() {
let file = create_temp_header("NSString *name;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsarray() {
let file = create_temp_header("NSArray *items;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsdictionary() {
let file = create_temp_header("NSDictionary *dict;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsobject() {
let file = create_temp_header("@interface MyClass : NSObject\n@end\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nsurl() {
let file = create_temp_header("NSURL *url;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nserror() {
let file = create_temp_header("NSError *error;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_string_literal() {
let file = create_temp_header("NSString *s = @\"hello\";\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_ns_type_nsstring() {
assert!(CppFormatter::contains_ns_type("NSString *name;"));
}
#[test]
fn test_contains_ns_type_nsarray() {
assert!(CppFormatter::contains_ns_type(
"NSArray<NSString *> *items;"
));
}
#[test]
fn test_contains_ns_type_at_line_start() {
assert!(CppFormatter::contains_ns_type("NSObject *obj;"));
}
#[test]
fn test_contains_ns_type_after_space() {
assert!(CppFormatter::contains_ns_type("id<NSCopying> obj;"));
}
#[test]
fn test_contains_ns_type_after_paren() {
assert!(CppFormatter::contains_ns_type("(NSString *)value"));
}
#[test]
fn test_contains_ns_type_no_false_positive_dns() {
assert!(!CppFormatter::contains_ns_type("DNSResolver resolver;"));
}
#[test]
fn test_contains_ns_type_no_false_positive_lowercase() {
assert!(!CppFormatter::contains_ns_type("namespace ns { }"));
}
#[test]
fn test_contains_ns_type_no_false_positive_part_of_word() {
assert!(!CppFormatter::contains_ns_type("AwesomeNSString x;"));
}
#[test]
fn test_contains_ns_type_pure_cpp() {
assert!(!CppFormatter::contains_ns_type(
"#include <vector>\nstd::vector<int> v;"
));
}
#[test]
fn test_contains_objc_syntax_array_literal() {
let file = create_temp_header("NSArray *arr = @[@\"a\", @\"b\"];\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_class_method() {
let file = create_temp_header("+ (instancetype)sharedInstance;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_instance_method() {
let file = create_temp_header("- (void)doSomething;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nullable() {
let file = create_temp_header("nullable NSString *name;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_nonnull() {
let file = create_temp_header("nonnull NSString *name;\n");
assert!(CppFormatter::contains_objc_syntax(file.path()));
}
#[test]
fn test_contains_objc_syntax_pure_cpp() {
let file = create_temp_header("#include <vector>\nstd::vector<int> v;\n");
assert!(!CppFormatter::contains_objc_syntax(file.path()));
}
}