use crate::checkers::Checker;
use crate::utils::types::{LintIssue, Severity};
use crate::{Language, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Default)]
pub struct CpplintConfig {
pub linelength: Option<u32>,
pub filter: Option<String>,
}
pub struct CppChecker {
config_path: Option<PathBuf>,
compile_commands_dir: Option<PathBuf>,
cpplint_cpp_config: CpplintConfig,
cpplint_oc_config: CpplintConfig,
cpp_ignored_checks: Vec<String>,
oc_ignored_checks: Vec<String>,
oc_fn_length: u32,
}
impl CppChecker {
fn config_search_dirs() -> Vec<PathBuf> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let git_root = crate::utils::get_project_root();
if cwd == git_root {
vec![cwd]
} else {
vec![cwd, git_root]
}
}
pub fn new() -> Self {
let (cpp_config, oc_config) = Self::load_cpplint_configs();
let clang_tidy_config = Self::find_plugin_clang_tidy_config();
let (cpp_ignored, oc_ignored) = Self::load_ignored_checks();
Self {
config_path: clang_tidy_config,
compile_commands_dir: None,
cpplint_cpp_config: cpp_config,
cpplint_oc_config: oc_config,
cpp_ignored_checks: cpp_ignored,
oc_ignored_checks: oc_ignored,
oc_fn_length: Self::load_oc_fn_length(),
}
}
fn find_plugin_clang_tidy_config() -> Option<PathBuf> {
let search_dirs = Self::config_search_dirs();
for dir in &search_dirs {
let cpp_clang_tidy = dir.join(".linthis/configs/cpp/.clang-tidy");
if cpp_clang_tidy.exists() {
return Some(cpp_clang_tidy);
}
}
for dir in &search_dirs {
let oc_clang_tidy = dir.join(".linthis/configs/oc/.clang-tidy");
if oc_clang_tidy.exists() {
return Some(oc_clang_tidy);
}
}
None
}
fn load_cpplint_configs() -> (CpplintConfig, CpplintConfig) {
use crate::config::Config;
let mut cpp_config = CpplintConfig {
linelength: Some(120),
filter: Some("-build/c++11,-build/c++14".to_string()),
};
let mut oc_config = CpplintConfig {
linelength: Some(150),
filter: Some("-build/c++11,-build/c++14,-build/header_guard,-build/include,-legal/copyright,-readability/casting,-runtime/references,-runtime/int,-whitespace/parens,-whitespace/braces,-whitespace/blank_line,-readability/braces,-whitespace/empty_if_body,-whitespace/operators".to_string()),
};
let search_dirs = Self::config_search_dirs();
for dir in &search_dirs {
let cpp_cfg_path = dir.join(".linthis/configs/cpp/CPPLINT.cfg");
if let Some(cfg) = Self::parse_cpplint_cfg(&cpp_cfg_path) {
if cfg.linelength.is_some() {
cpp_config.linelength = cfg.linelength;
}
if let Some(ref f) = cfg.filter {
cpp_config.filter = Some(Self::merge_filters(cpp_config.filter.as_deref(), f));
}
break;
}
}
for dir in &search_dirs {
let oc_cfg_path = dir.join(".linthis/configs/oc/CPPLINT.cfg");
if let Some(cfg) = Self::parse_cpplint_cfg(&oc_cfg_path) {
if cfg.linelength.is_some() {
oc_config.linelength = cfg.linelength;
}
if let Some(ref f) = cfg.filter {
oc_config.filter = Some(Self::merge_filters(oc_config.filter.as_deref(), f));
}
break;
}
}
let config_dir = search_dirs.first().cloned().unwrap_or_default();
let merged = Config::load_merged(&config_dir);
if let Some(ref cpp) = merged.language_overrides.cpp {
if cpp.linelength.is_some() {
cpp_config.linelength = cpp.linelength;
}
if cpp.cpplint_filter.is_some() {
cpp_config.filter = cpp.cpplint_filter.clone();
}
}
if let Some(ref oc) = merged.language_overrides.oc {
if oc.linelength.is_some() {
oc_config.linelength = oc.linelength;
}
if oc.cpplint_filter.is_some() {
oc_config.filter = oc.cpplint_filter.clone();
}
}
(cpp_config, oc_config)
}
fn load_ignored_checks() -> (Vec<String>, Vec<String>) {
use crate::config::Config;
let search_dirs = Self::config_search_dirs();
let config_dir = search_dirs.first().cloned().unwrap_or_default();
let merged = Config::load_merged(&config_dir);
let default_oc_ignored = vec![
"clang-analyzer-osx.cocoa.RetainCount".to_string(),
"clang-analyzer-osx.cocoa.Dealloc".to_string(),
];
let cpp_ignored = merged
.language_overrides
.cpp
.and_then(|c| c.clang_tidy_ignored_checks)
.unwrap_or_default();
let oc_ignored = merged
.language_overrides
.oc
.and_then(|c| c.clang_tidy_ignored_checks)
.unwrap_or(default_oc_ignored);
(cpp_ignored, oc_ignored)
}
fn load_oc_fn_length() -> u32 {
use crate::config::Config;
let search_dirs = Self::config_search_dirs();
let config_dir = search_dirs.first().cloned().unwrap_or_default();
let merged = Config::load_merged(&config_dir);
merged
.language_overrides
.oc
.and_then(|c| c.fn_length)
.unwrap_or(80)
}
fn merge_filters(base: Option<&str>, additional: &str) -> String {
use std::collections::HashSet;
let mut filters: HashSet<&str> = HashSet::new();
if let Some(base) = base {
for f in base.split(',') {
let f = f.trim();
if !f.is_empty() {
filters.insert(f);
}
}
}
for f in additional.split(',') {
let f = f.trim();
if !f.is_empty() {
filters.insert(f);
}
}
filters.into_iter().collect::<Vec<_>>().join(",")
}
fn parse_cpplint_cfg(path: &Path) -> Option<CpplintConfig> {
let content = std::fs::read_to_string(path).ok()?;
let mut linelength = None;
let mut filters = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(value) = line.strip_prefix("linelength=") {
linelength = value.trim().parse().ok();
} else if let Some(value) = line.strip_prefix("filter=") {
filters.push(value.trim().to_string());
}
}
let filter = if filters.is_empty() {
None
} else {
Some(filters.join(","))
};
Some(CpplintConfig { linelength, filter })
}
pub fn with_config(mut self, path: PathBuf) -> Self {
self.config_path = Some(path);
self
}
pub fn with_compile_commands_dir(mut self, path: PathBuf) -> Self {
self.compile_commands_dir = Some(path);
self
}
pub fn with_cpplint_cpp_config(mut self, config: CpplintConfig) -> Self {
self.cpplint_cpp_config = config;
self
}
pub fn with_cpplint_oc_config(mut self, config: CpplintConfig) -> Self {
self.cpplint_oc_config = config;
self
}
fn is_objective_c(path: &Path) -> bool {
crate::Language::from_path(path)
.map(|lang| lang == crate::Language::ObjectiveC)
.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);
}
let alt_config = current.join("_clang-tidy");
if alt_config.exists() {
return Some(alt_config);
}
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 {
let direct = current.join("compile_commands.json");
if direct.exists() {
return Some(current.clone());
}
for build_dir in &[
"build",
"Build",
"out",
"output",
"cmake-build-debug",
"cmake-build-release",
"cmake-build-relwithdebinfo",
"cmake-build-minsizerel",
".build",
"_build",
] {
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();
let is_build_dir = 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")
|| (depth > 0
&& (name_lower.contains("android")
|| name_lower.contains("ios")
|| name_lower.contains("linux")
|| name_lower.contains("windows")
|| name_lower.contains("macos")
|| name_lower.contains("darwin")
|| name_lower.contains("arm")
|| name_lower.contains("x86")
|| name_lower.contains("x64")
|| name_lower.contains("static")
|| name_lower.contains("shared")
|| name_lower.contains("debug")
|| name_lower.contains("release")));
if is_build_dir {
let compile_db = path.join("compile_commands.json");
if compile_db.exists() {
return Some(path);
}
if let Some(found) =
Self::find_compile_commands_recursive(&path, depth + 1, max_depth)
{
return Some(found);
}
}
}
None
}
fn has_clang_tidy() -> bool {
Command::new("clang-tidy")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn has_cpplint() -> bool {
Command::new("cpplint")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn run_clang_tidy(&self, path: &Path) -> Result<Vec<LintIssue>> {
if std::env::var("LINTHIS_SKIP_CLANG_TIDY").is_ok() {
return Ok(vec![]);
}
let abs_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().unwrap_or_default().join(path)
};
let project_root = crate::utils::get_project_root();
let mut cmd = Command::new("clang-tidy");
cmd.current_dir(&project_root);
cmd.arg(&abs_path);
if let Some(ref config) = self.config_path {
cmd.arg(format!("--config-file={}", config.display()));
} else if let Some(config) = Self::find_clang_tidy_config(&abs_path) {
cmd.arg(format!("--config-file={}", config.display()));
}
if let Some(ref build_path) = self.compile_commands_dir {
cmd.arg(format!("-p={}", build_path.display()));
} else if let Some(build_path) = Self::find_compile_commands(&abs_path) {
cmd.arg(format!("-p={}", build_path.display()));
} else {
cmd.arg("--");
}
let output = cmd.output().map_err(|e| {
crate::LintisError::checker("clang-tidy", path, format!("Failed to run: {}", e))
})?;
let stdout = String::from_utf8_lossy(&output.stdout);
let is_oc = Self::is_objective_c(path);
let ignored_checks = if is_oc {
&self.oc_ignored_checks
} else {
&self.cpp_ignored_checks
};
let issues = Self::parse_clang_tidy_output(&stdout, path, ignored_checks);
Ok(issues)
}
fn run_cpplint(&self, path: &Path) -> Result<Vec<LintIssue>> {
let mut cmd = Command::new("cpplint");
let is_oc = Self::is_objective_c(path);
let config = if is_oc {
&self.cpplint_oc_config
} else {
&self.cpplint_cpp_config
};
if is_oc {
cmd.arg("--extensions=m,mm,h");
}
if let Some(linelength) = config.linelength {
cmd.arg(format!("--linelength={}", linelength));
}
if let Some(ref filter) = config.filter {
cmd.arg(format!("--filter={}", filter));
}
cmd.arg(path);
let output = cmd.output().map_err(|e| {
crate::LintisError::checker("cpplint", path, format!("Failed to run: {}", e))
})?;
let stderr = String::from_utf8_lossy(&output.stderr);
let issues = Self::parse_cpplint_output(&stderr, path);
Ok(issues)
}
fn parse_clang_tidy_output(
output: &str,
file_path: &Path,
ignored_checks: &[String],
) -> Vec<LintIssue> {
let mut issues = Vec::new();
for line in output.lines() {
if let Some(issue) = Self::parse_clang_tidy_line(line, file_path) {
if let Some(ref code) = issue.code {
if ignored_checks.iter().any(|ignored| code == ignored) {
continue;
}
}
issues.push(issue);
}
}
issues
}
fn parse_clang_tidy_line(line: &str, default_path: &Path) -> Option<LintIssue> {
if !line.contains(": warning:") && !line.contains(": error:") {
return None;
}
let parts: Vec<&str> = line.splitn(5, ':').collect();
if parts.len() < 5 {
return None;
}
let file_path_parsed = std::path::PathBuf::from(parts[0]);
let path_str = file_path_parsed.to_string_lossy();
if path_str.contains("third_party")
|| path_str.contains("thirdparty")
|| path_str.contains("third-party")
|| path_str.contains("3rdparty")
|| path_str.contains("3rd_party")
|| path_str.contains("3rd-party")
|| path_str.contains("external")
|| path_str.contains("externals")
|| path_str.contains("vendor")
|| path_str.contains("node_modules")
{
return None;
}
let line_num = parts[1].trim().parse::<usize>().ok()?;
let col = parts[2].trim().parse::<usize>().ok();
let severity_str = parts[3].trim();
let message_part = parts[4].trim();
let severity = if severity_str.contains("error") {
Severity::Error
} else {
Severity::Warning
};
let (message, code) = if let Some(bracket_start) = message_part.rfind('[') {
let msg = message_part[..bracket_start].trim();
let check = message_part[bracket_start..]
.trim_matches(|c| c == '[' || c == ']')
.to_string();
(msg.to_string(), Some(check))
} else {
(message_part.to_string(), None)
};
if let Some(ref c) = code {
if c.starts_with("clang-diagnostic-") {
return None;
}
}
let file_path = if file_path_parsed.exists() {
file_path_parsed
} else {
default_path.to_path_buf()
};
let mut issue = LintIssue::new(file_path.clone(), line_num, message, severity)
.with_source("clang-tidy".to_string());
if let Some(c) = col {
issue = issue.with_column(c);
}
if let Some(c) = code {
issue = issue.with_code(c);
}
if let Some(ctx) = crate::utils::read_file_line_with_context(&file_path, line_num, 1) {
issue = issue
.with_code_line(ctx.line)
.with_context_before(ctx.before)
.with_context_after(ctx.after);
}
Some(issue)
}
fn parse_cpplint_output(output: &str, file_path: &Path) -> Vec<LintIssue> {
let mut issues = Vec::new();
for line in output.lines() {
if let Some(issue) = Self::parse_cpplint_line(line, file_path) {
issues.push(issue);
}
}
issues
}
fn parse_cpplint_line(line: &str, default_path: &Path) -> Option<LintIssue> {
if !line.contains(':')
|| line.starts_with("Done processing")
|| line.starts_with("Total errors")
{
return None;
}
if line.contains("Multi-line string") && line.contains("bogus warnings") {
return None;
}
let parts: Vec<&str> = line.splitn(3, ':').collect();
if parts.len() < 3 {
return None;
}
let file_path_parsed = std::path::PathBuf::from(parts[0]);
let line_num = parts[1].trim().parse::<usize>().ok()?;
let rest = parts[2].trim();
let (message, code) = if let Some(bracket_start) = rest.find('[') {
let msg = rest[..bracket_start].trim();
let category = rest[bracket_start..].trim_matches(|c| c == '[' || c == ']');
let cat = category.split("] [").next().unwrap_or(category);
(msg.to_string(), Some(cat.to_string()))
} else {
(rest.to_string(), None)
};
let severity = if message.to_lowercase().contains("error") {
Severity::Error
} else {
Severity::Warning
};
let file_path = if file_path_parsed.exists() {
file_path_parsed
} else {
default_path.to_path_buf()
};
let ctx = crate::utils::read_file_line_with_context(&file_path, line_num, 1);
if let (Some(ref cat), Some(ref context)) = (&code, &ctx) {
if cat.starts_with("whitespace/") {
let in_multiline_string = context.before.iter().any(|(_, line)| {
let trimmed = line.trim_end();
trimmed.ends_with('\\') || trimmed.ends_with("\\\"")
});
let line_trimmed = context.line.trim_end();
let looks_like_string_content = line_trimmed.ends_with('\\')
|| line_trimmed.contains(");\"") || line_trimmed.contains("();\"") || line_trimmed.contains("; \\") ;
if in_multiline_string || looks_like_string_content {
return None;
}
}
}
let mut issue = LintIssue::new(file_path.clone(), line_num, message, severity)
.with_source("cpplint".to_string());
if let Some(c) = code {
issue = issue.with_code(c);
}
if let Some(context) = ctx {
issue = issue
.with_code_line(context.line)
.with_context_before(context.before)
.with_context_after(context.after);
}
Some(issue)
}
pub(crate) fn count_sloc(lines: &[&str]) -> u32 {
let mut count = 0u32;
let mut in_block_comment = false;
for line in lines {
let trimmed = line.trim();
if in_block_comment {
if trimmed.contains("*/") {
in_block_comment = false;
}
continue;
}
if trimmed.is_empty() || trimmed.starts_with("//") {
continue;
}
if trimmed == "{" || trimmed == "}" {
continue;
}
if trimmed.starts_with("/*") {
if trimmed.contains("*/") {
continue;
}
in_block_comment = true;
continue;
}
count += 1;
}
count
}
pub(crate) fn extract_method_name(signature: &str) -> String {
let after_return = match signature.find(')') {
Some(i) => signature[i + 1..].trim(),
None => return signature.to_string(),
};
let mut selector = String::new();
let mut word = String::new();
let mut chars = after_return.chars().peekable();
while let Some(c) = chars.next() {
match c {
'{' => break,
'(' => {
word.clear();
let mut depth = 1usize;
for inner in chars.by_ref() {
match inner {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
}
':' => {
let kw = word.trim().to_string();
if !kw.is_empty() {
selector.push_str(&kw);
selector.push(':');
}
word.clear();
}
c if c.is_ascii_whitespace() => {
if !selector.is_empty() {
word.clear();
}
}
c => word.push(c),
}
}
if selector.is_empty() {
word.trim().to_string()
} else {
selector
}
}
pub(crate) fn check_objc_method_lengths(
content: &str,
path: &Path,
threshold: u32,
) -> Vec<LintIssue> {
let lines: Vec<&str> = content.lines().collect();
let mut issues = Vec::new();
let mut method_starts: Vec<(usize, String)> = Vec::new();
for (idx, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if (trimmed.starts_with("- (")
|| trimmed.starts_with("+ (")
|| trimmed.starts_with("-(")
|| trimmed.starts_with("+("))
&& !trimmed.ends_with(';')
{
let name = Self::extract_method_name(trimmed);
method_starts.push((idx + 1, name)); }
}
for (i, (start_line, name)) in method_starts.iter().enumerate() {
let body_start_idx = *start_line; let body_end_idx = if i + 1 < method_starts.len() {
method_starts[i + 1].0 - 1 } else {
lines.len()
};
let body = if body_start_idx < body_end_idx {
&lines[body_start_idx..body_end_idx]
} else {
&lines[0..0]
};
let sloc = Self::count_sloc(body);
if sloc > threshold {
let message = format!(
"Method '{}' has {} lines of code (limit is {}) [readability/fn_size]",
name, sloc, threshold
);
let issue =
LintIssue::new(path.to_path_buf(), *start_line, message, Severity::Warning)
.with_code("readability/fn_size".to_string())
.with_source("objc-method-length".to_string());
issues.push(issue);
}
}
issues
}
fn run_objc_method_length(&self, path: &Path) -> Result<Vec<LintIssue>> {
let content = std::fs::read_to_string(path).map_err(|e| {
crate::LintisError::checker(
"objc-method-length",
path,
format!("Failed to read file: {}", e),
)
})?;
Ok(Self::check_objc_method_lengths(
&content,
path,
self.oc_fn_length,
))
}
}
impl Default for CppChecker {
fn default() -> Self {
Self::new()
}
}
impl Checker for CppChecker {
fn name(&self) -> &str {
match (Self::has_clang_tidy(), Self::has_cpplint()) {
(true, true) => "clang-tidy+cpplint",
(true, false) => "clang-tidy",
(false, true) => "cpplint",
(false, false) => "cpp-checker",
}
}
fn supported_languages(&self) -> &[Language] {
&[Language::Cpp, Language::ObjectiveC]
}
fn check(&self, path: &Path) -> Result<Vec<LintIssue>> {
let mut all_issues = Vec::new();
if Self::has_clang_tidy() {
match self.run_clang_tidy(path) {
Ok(issues) => all_issues.extend(issues),
Err(e) => {
log::warn!("clang-tidy failed: {}", e);
}
}
}
if Self::has_cpplint() {
match self.run_cpplint(path) {
Ok(issues) => all_issues.extend(issues),
Err(e) => {
log::warn!("cpplint failed: {}", e);
}
}
}
if Self::is_objective_c(path) {
match self.run_objc_method_length(path) {
Ok(method_issues) => all_issues.extend(method_issues),
Err(e) => log::warn!("objc method length check failed: {}", e),
}
}
all_issues.sort_by(|a, b| {
(&a.file_path, a.line, &a.message).cmp(&(&b.file_path, b.line, &b.message))
});
all_issues.dedup_by(|a, b| {
a.file_path == b.file_path && a.line == b.line && a.message == b.message
});
Ok(all_issues)
}
fn is_available(&self) -> bool {
Self::has_clang_tidy() || Self::has_cpplint()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_merge_filters_both_present() {
let result =
CppChecker::merge_filters(Some("-build/c++11,-build/c++14"), "-whitespace/tab");
assert!(result.contains("-build/c++11"));
assert!(result.contains("-build/c++14"));
assert!(result.contains("-whitespace/tab"));
}
#[test]
fn test_merge_filters_base_none() {
let result = CppChecker::merge_filters(None, "-build/c++11,-whitespace/tab");
assert!(result.contains("-build/c++11"));
assert!(result.contains("-whitespace/tab"));
}
#[test]
fn test_merge_filters_removes_duplicates() {
let result =
CppChecker::merge_filters(Some("-build/c++11"), "-build/c++11,-whitespace/tab");
let count = result.matches("-build/c++11").count();
assert_eq!(count, 1);
assert!(result.contains("-whitespace/tab"));
}
#[test]
fn test_merge_filters_trims_whitespace() {
let result =
CppChecker::merge_filters(Some(" -build/c++11 , -build/c++14 "), " -whitespace/tab ");
assert!(result.contains("-build/c++11"));
assert!(result.contains("-build/c++14"));
assert!(result.contains("-whitespace/tab"));
}
#[test]
fn test_merge_filters_empty_strings() {
let result = CppChecker::merge_filters(Some(""), "");
assert!(result.is_empty());
}
fn create_temp_cfg(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
file.flush().unwrap();
file
}
#[test]
fn test_parse_cpplint_cfg_linelength() {
let file = create_temp_cfg("linelength=120\n");
let config = CppChecker::parse_cpplint_cfg(file.path()).unwrap();
assert_eq!(config.linelength, Some(120));
assert!(config.filter.is_none());
}
#[test]
fn test_parse_cpplint_cfg_filter() {
let file = create_temp_cfg("filter=-build/c++11,-whitespace/tab\n");
let config = CppChecker::parse_cpplint_cfg(file.path()).unwrap();
assert!(config.linelength.is_none());
assert_eq!(
config.filter,
Some("-build/c++11,-whitespace/tab".to_string())
);
}
#[test]
fn test_parse_cpplint_cfg_both() {
let file = create_temp_cfg("linelength=100\nfilter=-build/header_guard\n");
let config = CppChecker::parse_cpplint_cfg(file.path()).unwrap();
assert_eq!(config.linelength, Some(100));
assert_eq!(config.filter, Some("-build/header_guard".to_string()));
}
#[test]
fn test_parse_cpplint_cfg_multiple_filters() {
let file = create_temp_cfg("filter=-build/c++11\nfilter=-whitespace/tab\n");
let config = CppChecker::parse_cpplint_cfg(file.path()).unwrap();
let filter = config.filter.unwrap();
assert!(filter.contains("-build/c++11"));
assert!(filter.contains("-whitespace/tab"));
}
#[test]
fn test_parse_cpplint_cfg_with_comments() {
let file = create_temp_cfg("# This is a comment\nlinelength=80\n# Another comment\n");
let config = CppChecker::parse_cpplint_cfg(file.path()).unwrap();
assert_eq!(config.linelength, Some(80));
}
#[test]
fn test_parse_cpplint_cfg_empty_lines() {
let file = create_temp_cfg("\n\nlinelength=150\n\n");
let config = CppChecker::parse_cpplint_cfg(file.path()).unwrap();
assert_eq!(config.linelength, Some(150));
}
#[test]
fn test_parse_cpplint_cfg_nonexistent_file() {
let result = CppChecker::parse_cpplint_cfg(Path::new("/nonexistent/path/CPPLINT.cfg"));
assert!(result.is_none());
}
#[test]
fn test_parse_clang_tidy_warning() {
let line = "test.cpp:10:5: warning: use nullptr [modernize-use-nullptr]";
let default_path = Path::new("default.cpp");
let issue = CppChecker::parse_clang_tidy_line(line, default_path).unwrap();
assert_eq!(issue.line, 10);
assert_eq!(issue.column, Some(5));
assert_eq!(issue.severity, Severity::Warning);
assert!(issue.message.contains("use nullptr"));
assert_eq!(issue.code, Some("modernize-use-nullptr".to_string()));
assert_eq!(issue.source, Some("clang-tidy".to_string()));
}
#[test]
fn test_parse_clang_tidy_error() {
let line = "main.cpp:5:1: error: no matching function for call [misc-error]";
let default_path = Path::new("default.cpp");
let issue = CppChecker::parse_clang_tidy_line(line, default_path).unwrap();
assert_eq!(issue.line, 5);
assert_eq!(issue.column, Some(1));
assert_eq!(issue.severity, Severity::Error);
assert!(issue.message.contains("no matching function"));
assert_eq!(issue.code, Some("misc-error".to_string()));
}
#[test]
fn test_parse_clang_tidy_clang_diagnostic_filtered() {
let line = "main.cpp:5:1: error: unknown type name 'foo' [clang-diagnostic-error]";
let default_path = Path::new("default.cpp");
let result = CppChecker::parse_clang_tidy_line(line, default_path);
assert!(result.is_none());
}
#[test]
fn test_parse_clang_tidy_no_bracket() {
let line = "test.cpp:20:3: warning: some warning without bracket";
let default_path = Path::new("default.cpp");
let issue = CppChecker::parse_clang_tidy_line(line, default_path).unwrap();
assert_eq!(issue.line, 20);
assert!(issue.code.is_none());
assert!(issue.message.contains("some warning without bracket"));
}
#[test]
fn test_parse_clang_tidy_irrelevant_line() {
let line = "In file included from test.cpp:1:";
let default_path = Path::new("default.cpp");
let result = CppChecker::parse_clang_tidy_line(line, default_path);
assert!(result.is_none());
}
#[test]
fn test_parse_clang_tidy_note_line() {
let line = "test.cpp:10:5: note: previous declaration is here";
let default_path = Path::new("default.cpp");
let result = CppChecker::parse_clang_tidy_line(line, default_path);
assert!(result.is_none()); }
#[test]
fn test_parse_cpplint_standard_warning() {
let line = "test.cpp:10: Missing space after comma [whitespace/comma] [3]";
let default_path = Path::new("default.cpp");
let issue = CppChecker::parse_cpplint_line(line, default_path).unwrap();
assert_eq!(issue.line, 10);
assert_eq!(issue.severity, Severity::Warning);
assert!(issue.message.contains("Missing space after comma"));
assert_eq!(issue.code, Some("whitespace/comma".to_string()));
assert_eq!(issue.source, Some("cpplint".to_string()));
}
#[test]
fn test_parse_cpplint_header_guard() {
let line = "test.h:0: No #ifndef header guard found, suggested CPP variable is: TEST_H_ [build/header_guard] [5]";
let default_path = Path::new("test.h");
let issue = CppChecker::parse_cpplint_line(line, default_path).unwrap();
assert_eq!(issue.line, 0);
assert!(issue.message.contains("header guard"));
assert_eq!(issue.code, Some("build/header_guard".to_string()));
}
#[test]
fn test_parse_cpplint_endif_comment() {
let line =
r##"test.h:50: #endif line should be "#endif // TEST_H_" [build/header_guard] [5]"##;
let default_path = Path::new("test.h");
let issue = CppChecker::parse_cpplint_line(line, default_path).unwrap();
assert_eq!(issue.line, 50);
assert!(issue.message.contains("#endif"));
assert_eq!(issue.code, Some("build/header_guard".to_string()));
}
#[test]
fn test_parse_cpplint_line_length() {
let line =
"main.cpp:25: Lines should be <= 120 characters long [whitespace/line_length] [2]";
let default_path = Path::new("main.cpp");
let issue = CppChecker::parse_cpplint_line(line, default_path).unwrap();
assert_eq!(issue.line, 25);
assert!(issue.message.contains("120 characters"));
assert_eq!(issue.code, Some("whitespace/line_length".to_string()));
}
#[test]
fn test_parse_cpplint_done_processing() {
let line = "Done processing test.cpp";
let default_path = Path::new("test.cpp");
let result = CppChecker::parse_cpplint_line(line, default_path);
assert!(result.is_none());
}
#[test]
fn test_parse_cpplint_total_errors() {
let line = "Total errors found: 5";
let default_path = Path::new("test.cpp");
let result = CppChecker::parse_cpplint_line(line, default_path);
assert!(result.is_none());
}
#[test]
fn test_parse_cpplint_comment_spacing() {
let line =
"test.cpp:15: Should have a space between // and comment [whitespace/comments] [4]";
let default_path = Path::new("test.cpp");
let issue = CppChecker::parse_cpplint_line(line, default_path).unwrap();
assert_eq!(issue.line, 15);
assert!(issue.message.contains("space between //"));
assert_eq!(issue.code, Some("whitespace/comments".to_string()));
}
#[test]
fn test_parse_cpplint_multiline_string_filtered() {
let line = r#"test.m:710: Multi-line string ("...") found. This lint script doesn't do well with such strings, and may give bogus warnings. Use C++11 raw strings or concatenation instead. [readability/multiline_string] [5]"#;
let default_path = Path::new("test.m");
let result = CppChecker::parse_cpplint_line(line, default_path);
assert!(result.is_none());
}
#[test]
fn test_cpplint_config_default() {
let config = CpplintConfig::default();
assert!(config.linelength.is_none());
assert!(config.filter.is_none());
}
#[test]
fn test_cpp_checker_with_config() {
let checker = CppChecker::new().with_config(PathBuf::from("/custom/.clang-tidy"));
assert_eq!(
checker.config_path,
Some(PathBuf::from("/custom/.clang-tidy"))
);
}
#[test]
fn test_cpp_checker_with_compile_commands_dir() {
let checker = CppChecker::new().with_compile_commands_dir(PathBuf::from("/build"));
assert_eq!(checker.compile_commands_dir, Some(PathBuf::from("/build")));
}
#[test]
fn test_cpp_checker_with_cpplint_cpp_config() {
let config = CpplintConfig {
linelength: Some(80),
filter: Some("-build/c++11".to_string()),
};
let checker = CppChecker::new().with_cpplint_cpp_config(config.clone());
assert_eq!(checker.cpplint_cpp_config.linelength, Some(80));
}
#[test]
fn test_cpp_checker_with_cpplint_oc_config() {
let config = CpplintConfig {
linelength: Some(200),
filter: Some("-whitespace/parens".to_string()),
};
let checker = CppChecker::new().with_cpplint_oc_config(config);
assert_eq!(checker.cpplint_oc_config.linelength, Some(200));
}
#[test]
fn test_cpp_checker_default_oc_fn_length() {
let checker = CppChecker::new();
assert_eq!(checker.oc_fn_length, 80);
}
#[test]
fn test_count_sloc_plain_code() {
let lines = vec!["int x = 1;", "int y = 2;", "return x + y;"];
assert_eq!(CppChecker::count_sloc(&lines), 3);
}
#[test]
fn test_count_sloc_skips_blank_lines() {
let lines = vec!["int x = 1;", "", " ", "return x;"];
assert_eq!(CppChecker::count_sloc(&lines), 2);
}
#[test]
fn test_count_sloc_skips_line_comments() {
let lines = vec!["// comment", "int x = 1;", "// another"];
assert_eq!(CppChecker::count_sloc(&lines), 1);
}
#[test]
fn test_count_sloc_skips_single_line_block_comment() {
let lines = vec!["/* inline comment */", "int x = 1;"];
assert_eq!(CppChecker::count_sloc(&lines), 1);
}
#[test]
fn test_count_sloc_skips_multiline_block_comment() {
let lines = vec!["/*", " * block comment", " */", "int x = 1;"];
assert_eq!(CppChecker::count_sloc(&lines), 1);
}
#[test]
fn test_count_sloc_trailing_comment_counts_as_code() {
let lines = vec!["int x = 1; // set x"];
assert_eq!(CppChecker::count_sloc(&lines), 1);
}
#[test]
fn test_extract_method_name_simple() {
assert_eq!(
CppChecker::extract_method_name("- (void)viewDidLoad {"),
"viewDidLoad"
);
}
#[test]
fn test_extract_method_name_with_single_arg() {
assert_eq!(
CppChecker::extract_method_name("- (NSString *)stringForKey:(NSString *)key"),
"stringForKey:"
);
}
#[test]
fn test_extract_method_name_class_method() {
assert_eq!(
CppChecker::extract_method_name("+ (instancetype)sharedInstance"),
"sharedInstance"
);
}
#[test]
fn test_extract_method_name_multi_arg() {
assert_eq!(
CppChecker::extract_method_name(
"- (void)tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)ip {"
),
"tableView:didSelectRowAtIndexPath:"
);
}
fn make_objc_content_with_sloc(method_name: &str, sloc: usize) -> String {
let mut lines = vec![format!("- (void){} {{", method_name)];
for i in 0..sloc {
lines.push(format!(" int var{} = {};", i, i));
}
lines.push("}".to_string());
lines.join("\n")
}
#[test]
fn test_check_objc_method_lengths_under_threshold_no_issue() {
let content = make_objc_content_with_sloc("shortMethod", 5);
let issues =
CppChecker::check_objc_method_lengths(&content, std::path::Path::new("test.m"), 80);
assert!(
issues.is_empty(),
"Expected no issues for 5 SLOC, got: {:?}",
issues
);
}
#[test]
fn test_check_objc_method_lengths_over_threshold_reports_issue() {
let content = make_objc_content_with_sloc("longMethod", 85);
let issues =
CppChecker::check_objc_method_lengths(&content, std::path::Path::new("test.m"), 80);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].line, 1);
assert!(
issues[0].message.contains("longMethod"),
"message: {}",
issues[0].message
);
assert!(
issues[0].message.contains("readability/fn_size"),
"message: {}",
issues[0].message
);
assert!(
issues[0].message.contains("85 lines of code"),
"Expected '85 lines of code' in message: {}",
issues[0].message
);
assert_eq!(issues[0].code.as_deref(), Some("readability/fn_size"));
assert_eq!(issues[0].source.as_deref(), Some("objc-method-length"));
}
#[test]
fn test_check_objc_method_lengths_exactly_at_threshold_no_issue() {
let content = make_objc_content_with_sloc("boundaryMethod", 80);
let issues =
CppChecker::check_objc_method_lengths(&content, std::path::Path::new("test.m"), 80);
assert!(
issues.is_empty(),
"Expected no issue at exactly threshold, got: {:?}",
issues
);
}
#[test]
fn test_check_objc_method_lengths_blank_and_comments_not_counted() {
let mut lines = vec!["- (void)almostLongMethod {".to_string()];
for i in 0..79 {
lines.push(format!(" int var{} = {};", i, i));
if i % 4 == 0 {
lines.push(String::new());
lines.push(" // a comment".to_string());
}
}
lines.push("}".to_string());
let content = lines.join("\n");
let issues =
CppChecker::check_objc_method_lengths(&content, std::path::Path::new("test.mm"), 80);
assert!(
issues.is_empty(),
"Expected no issues (79 SLOC), got: {:?}",
issues
);
}
#[test]
fn test_check_objc_method_lengths_multiple_methods_each_checked() {
let mut lines = vec!["- (void)shortMethod {".to_string()];
for i in 0..5 {
lines.push(format!(" int a{} = {};", i, i));
}
lines.push("}".to_string());
let long_method_line = lines.len() + 1; lines.push("- (void)longMethod {".to_string());
for i in 0..85 {
lines.push(format!(" int b{} = {};", i, i));
}
lines.push("}".to_string());
let content = lines.join("\n");
let issues =
CppChecker::check_objc_method_lengths(&content, std::path::Path::new("test.m"), 80);
assert_eq!(issues.len(), 1, "Expected 1 issue, got: {:?}", issues);
assert!(
issues[0].message.contains("longMethod"),
"message: {}",
issues[0].message
);
assert_eq!(
issues[0].line, long_method_line,
"Expected issue at line {}",
long_method_line
);
}
#[test]
fn test_check_objc_method_lengths_custom_threshold() {
let content = make_objc_content_with_sloc("mediumMethod", 50);
let issues =
CppChecker::check_objc_method_lengths(&content, std::path::Path::new("test.mm"), 30);
assert_eq!(issues.len(), 1);
assert!(issues[0].message.contains("mediumMethod"));
assert_eq!(issues[0].code.as_deref(), Some("readability/fn_size"));
assert_eq!(issues[0].source.as_deref(), Some("objc-method-length"));
}
}