use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct CheckerDiagnostic {
pub checker: String,
pub rule_id: String,
pub severity: DiagLevel,
pub message: String,
pub file: String,
pub line: usize,
pub column: usize,
pub fixit: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagLevel {
Note,
Warning,
Error,
Fatal,
}
impl DiagLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Note => "note",
Self::Warning => "warning",
Self::Error => "error",
Self::Fatal => "fatal",
}
}
}
#[derive(Debug, Clone)]
pub struct CheckerResult {
pub checker_name: String,
pub diagnostics: Vec<CheckerDiagnostic>,
pub files_checked: usize,
}
impl CheckerResult {
pub fn new(name: &str) -> Self {
Self {
checker_name: name.to_string(),
diagnostics: Vec::new(),
files_checked: 0,
}
}
}
pub trait StaticChecker {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn check(&self, source: &str) -> CheckerResult;
}
#[derive(Debug, Clone)]
pub struct CppCoreGuidelinesChecker {
pub enabled_rules: HashSet<String>,
}
impl CppCoreGuidelinesChecker {
pub fn new() -> Self {
let mut rules = HashSet::new();
for r in &[
"C.128", "ES.23", "I.3", "F.6", "C.35", "C.36", "C.45", "C.46", "F.15", "F.16",
"ES.20", "ES.21", "ES.56", "Con.1", "Con.2", "Con.3", "Con.4",
] {
rules.insert(r.to_string());
}
Self {
enabled_rules: rules,
}
}
pub fn enable_rule(&mut self, rule: &str) {
self.enabled_rules.insert(rule.to_string());
}
pub fn disable_rule(&mut self, rule: &str) {
self.enabled_rules.remove(rule);
}
pub fn check_c128(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.contains("virtual") && trimmed.contains("override") {
diags.push(CheckerDiagnostic {
checker: "cppcoreguidelines".into(), rule_id: "C.128".into(),
severity: DiagLevel::Warning, file: "input.cpp".into(), line: i + 1, column: 1,
message: "Virtual function should specify exactly one of virtual, override, or final (C.128)".into(),
fixit: Some("consider removing 'virtual' when 'override' is used".into()),
});
}
}
diags
}
pub fn check_es23(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains(" = 0")
&& !line.contains(" = 0;")
&& line.contains("int")
&& !line.contains("{")
{
diags.push(CheckerDiagnostic {
checker: "cppcoreguidelines".into(),
rule_id: "ES.23".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Prefer {} initializer syntax over = (ES.23)".into(),
fixit: Some("use {0} instead of = 0".into()),
});
}
}
diags
}
pub fn check_i3(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("static") && source.contains("getInstance") {
diags.push(CheckerDiagnostic {
checker: "cppcoreguidelines".into(),
rule_id: "I.3".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Avoid singletons; consider dependency injection instead (I.3)".into(),
fixit: None,
});
}
diags
}
pub fn check_f6(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("// precondition:")
|| line.trim().starts_with("// requires:")
{
diags.push(CheckerDiagnostic {
checker: "cppcoreguidelines".into(),
rule_id: "F.6".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Prefer using assert() for preconditions instead of comments (F.6)"
.into(),
fixit: Some("replace comment with assert(condition)".into()),
});
}
}
diags
}
pub fn check_c35(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("~") && source.contains("virtual") {
if !source.contains("public") && !source.contains("protected") {
diags.push(CheckerDiagnostic {
checker: "cppcoreguidelines".into(), rule_id: "C.35".into(),
severity: DiagLevel::Warning, file: "input.cpp".into(), line: 1, column: 1,
message: "Base class destructor should be public and virtual, or protected and non-virtual (C.35)".into(),
fixit: Some("add 'public virtual' or 'protected' to destructor".into()),
});
}
}
diags
}
}
impl StaticChecker for CppCoreGuidelinesChecker {
fn name(&self) -> &str {
"cppcoreguidelines"
}
fn description(&self) -> &str {
"Checks C++ Core Guidelines violations"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result.diagnostics.extend(self.check_c128(source));
result.diagnostics.extend(self.check_es23(source));
result.diagnostics.extend(self.check_i3(source));
result.diagnostics.extend(self.check_f6(source));
result.diagnostics.extend(self.check_c35(source));
result.files_checked = 1;
result
}
}
impl Default for CppCoreGuidelinesChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LockAnnotation {
GuardedBy(String),
PtGuardedBy(String),
AcquiredBefore(String),
AcquiredAfter(String),
ExclusiveLockFunction,
SharedLockFunction,
UnlockFunction,
ExcludeLockFunction,
NoThreadSafetyAnalysis,
}
impl LockAnnotation {
pub fn from_attribute(s: &str) -> Option<Self> {
if s.starts_with("guarded_by(") {
let inner = s.trim_start_matches("guarded_by(").trim_end_matches(')');
Some(Self::GuardedBy(inner.to_string()))
} else if s.starts_with("pt_guarded_by(") {
let inner = s.trim_start_matches("pt_guarded_by(").trim_end_matches(')');
Some(Self::PtGuardedBy(inner.to_string()))
} else if s.starts_with("acquired_before(") {
let inner = s
.trim_start_matches("acquired_before(")
.trim_end_matches(')');
Some(Self::AcquiredBefore(inner.to_string()))
} else if s.starts_with("acquired_after(") {
let inner = s
.trim_start_matches("acquired_after(")
.trim_end_matches(')');
Some(Self::AcquiredAfter(inner.to_string()))
} else {
match s {
"exclusive_lock_function" => Some(Self::ExclusiveLockFunction),
"shared_lock_function" => Some(Self::SharedLockFunction),
"unlock_function" => Some(Self::UnlockFunction),
"exclude_lock_function" => Some(Self::ExcludeLockFunction),
"no_thread_safety_analysis" => Some(Self::NoThreadSafetyAnalysis),
_ => None,
}
}
}
pub fn is_lock_acquire(&self) -> bool {
matches!(self, Self::ExclusiveLockFunction | Self::SharedLockFunction)
}
}
#[derive(Debug, Clone)]
pub struct ThreadSafetyChecker {
pub mutexes: HashMap<String, MutexDescriptor>,
pub check_double_lock: bool,
pub check_unlock_without_lock: bool,
}
#[derive(Debug, Clone)]
pub struct MutexDescriptor {
pub name: String,
pub locked: bool,
pub locked_shared: bool,
}
impl ThreadSafetyChecker {
pub fn new() -> Self {
Self {
mutexes: HashMap::new(),
check_double_lock: true,
check_unlock_without_lock: true,
}
}
pub fn register_mutex(&mut self, name: &str) {
self.mutexes.insert(
name.to_string(),
MutexDescriptor {
name: name.to_string(),
locked: false,
locked_shared: false,
},
);
}
pub fn check_lock_order(&self, _acquired: &str, _held: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
diags
}
pub fn check_double_lock(&self, mutex_name: &str, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let mut lock_count = 0;
for line in source.lines() {
if line.contains(&format!("{}.lock()", mutex_name))
|| line.contains(&format!("lock_guard<{}>", mutex_name))
{
lock_count += 1;
}
}
if lock_count > 1 {
diags.push(CheckerDiagnostic {
checker: "threadsafety".into(),
rule_id: "double_lock".into(),
severity: DiagLevel::Error,
file: "input.cpp".into(),
line: 1,
column: 1,
message: format!(
"Mutex '{}' locked twice without intervening unlock",
mutex_name
),
fixit: None,
});
}
diags
}
pub fn check_guarded_access(&self, var: &str, _source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
diags.push(CheckerDiagnostic {
checker: "threadsafety".into(),
rule_id: "guarded_access".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: 1,
column: 1,
message: format!("Access to guarded variable '{}' outside lock", var),
fixit: Some("acquire the mutex before accessing".into()),
});
diags
}
}
impl StaticChecker for ThreadSafetyChecker {
fn name(&self) -> &str {
"threadsafety"
}
fn description(&self) -> &str {
"Checks mutex locking discipline with annotations"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
for (name, _) in &self.mutexes {
result
.diagnostics
.extend(self.check_double_lock(name, source));
}
if source.contains("mutex") && !source.contains("lock") {
result.diagnostics.push(CheckerDiagnostic {
checker: "threadsafety".into(),
rule_id: "missing_lock".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Mutex variable not locked before use".into(),
fixit: Some("add std::lock_guard".into()),
});
}
result.files_checked = 1;
result
}
}
impl Default for ThreadSafetyChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PerformanceAnalyzer {
pub detect_unnecessary_copies: bool,
pub detect_pass_by_value: bool,
pub prefer_emplace: bool,
}
impl PerformanceAnalyzer {
pub fn new() -> Self {
Self {
detect_unnecessary_copies: true,
detect_pass_by_value: true,
prefer_emplace: true,
}
}
pub fn check_unnecessary_copies(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("auto ")
&& line.contains(" = ")
&& !line.contains("&")
&& !line.contains("&&")
&& !line.contains("std::move")
{
diags.push(CheckerDiagnostic {
checker: "performance".into(),
rule_id: "unnecessary_copy".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Unnecessary copy detected; consider using const auto&".into(),
fixit: Some("use 'const auto&' instead of 'auto'".into()),
});
}
}
diags
}
pub fn check_pass_by_value_sink(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("const std::string&") && line.contains(")") {
diags.push(CheckerDiagnostic {
checker: "performance".into(),
rule_id: "pass_by_value_sink".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Consider pass-by-value + std::move for sink parameters".into(),
fixit: Some("use 'std::string' instead of 'const std::string&'".into()),
});
}
}
diags
}
pub fn check_emplace_vs_push(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("push_back(std::make_pair")
|| source.contains("push_back(std::make_tuple")
{
diags.push(CheckerDiagnostic {
checker: "performance".into(),
rule_id: "emplace_back".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Use emplace_back instead of push_back with make_pair/make_tuple".into(),
fixit: Some("replace push_back(make_pair(a,b)) with emplace_back(a,b)".into()),
});
}
if source.contains("push_back(") && source.contains("}") && !source.contains("emplace_back")
{
diags.push(CheckerDiagnostic {
checker: "performance".into(),
rule_id: "emplace_suggestion".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Consider emplace_back() for constructing elements in-place".into(),
fixit: Some("replace push_back(...) with emplace_back(...)".into()),
});
}
diags
}
}
impl StaticChecker for PerformanceAnalyzer {
fn name(&self) -> &str {
"performance"
}
fn description(&self) -> &str {
"Detects unnecessary copies, pass-by-value opportunities, and emplace suggestions"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result
.diagnostics
.extend(self.check_unnecessary_copies(source));
result
.diagnostics
.extend(self.check_pass_by_value_sink(source));
result
.diagnostics
.extend(self.check_emplace_vs_push(source));
result.files_checked = 1;
result
}
}
impl Default for PerformanceAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ModernizeChecker {
pub suggest_nullptr: bool,
pub suggest_override: bool,
pub suggest_auto: bool,
pub suggest_range_for: bool,
}
impl ModernizeChecker {
pub fn new() -> Self {
Self {
suggest_nullptr: true,
suggest_override: true,
suggest_auto: true,
suggest_range_for: true,
}
}
pub fn check_nullptr(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("NULL") || line.contains(" = 0") && line.contains("*") {
diags.push(CheckerDiagnostic {
checker: "modernize".into(),
rule_id: "nullptr".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Use nullptr instead of NULL or 0 for null pointers".into(),
fixit: Some("replace with nullptr".into()),
});
}
}
diags
}
pub fn check_override(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("virtual") && line.contains("override") {
diags.push(CheckerDiagnostic {
checker: "modernize".into(),
rule_id: "redundant_virtual".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "'virtual' is redundant when 'override' is used".into(),
fixit: Some("remove 'virtual'".into()),
});
}
}
diags
}
pub fn check_auto(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("std::vector<")
&& line.contains("::iterator")
&& !line.contains("auto")
{
diags.push(CheckerDiagnostic {
checker: "modernize".into(),
rule_id: "use_auto".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Use auto for iterator type deduction".into(),
fixit: Some("replace 'std::vector<T>::iterator' with 'auto'".into()),
});
}
}
diags
}
pub fn check_range_for(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("for (")
&& source.contains("::begin()")
&& source.contains("::end()")
&& !source.contains(":")
{
diags.push(CheckerDiagnostic {
checker: "modernize".into(),
rule_id: "range_for".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Use range-based for loop instead of iterator pair".into(),
fixit: Some("replace with 'for (auto& x : container)'".into()),
});
}
diags
}
}
impl StaticChecker for ModernizeChecker {
fn name(&self) -> &str {
"modernize"
}
fn description(&self) -> &str {
"Suggests modern C++ replacements (nullptr, auto, override, range-for)"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result.diagnostics.extend(self.check_nullptr(source));
result.diagnostics.extend(self.check_override(source));
result.diagnostics.extend(self.check_auto(source));
result.diagnostics.extend(self.check_range_for(source));
result.files_checked = 1;
result
}
}
impl Default for ModernizeChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PortabilityChecker {
pub check_type_sizes: bool,
pub check_alignment: bool,
pub check_endianness: bool,
pub check_undefined_behavior: bool,
}
impl PortabilityChecker {
pub fn new() -> Self {
Self {
check_type_sizes: true,
check_alignment: true,
check_endianness: true,
check_undefined_behavior: true,
}
}
pub fn check_type_sizes(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("sizeof(int)") {
diags.push(CheckerDiagnostic {
checker: "portability".into(),
rule_id: "type_size".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "sizeof(int) may vary across platforms; use fixed-width types".into(),
fixit: Some("use int32_t from <cstdint>".into()),
});
}
diags
}
pub fn check_alignment(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("reinterpret_cast") && source.contains("char*") {
diags.push(CheckerDiagnostic {
checker: "portability".into(),
rule_id: "alignment".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "reinterpret_cast to char* may violate alignment requirements".into(),
fixit: Some("use std::bit_cast or memcpy for type punning".into()),
});
}
diags
}
pub fn check_endianness(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("htonl") || source.contains("ntohl") {
diags.push(CheckerDiagnostic {
checker: "portability".into(),
rule_id: "endianness".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Endianness conversion detected; ensure portability".into(),
fixit: None,
});
}
diags
}
pub fn check_undefined_behavior(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let ub_patterns = [
("<< 32", "shift amount >= width of type"),
(">> -1", "right shift by negative amount"),
("[", "potential buffer overflow, check bounds"),
];
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
for (pattern, msg) in &ub_patterns {
if line.contains(pattern) {
diags.push(CheckerDiagnostic {
checker: "portability".into(),
rule_id: "undefined_behavior".into(),
severity: DiagLevel::Error,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: format!("Potential undefined behavior: {}", msg),
fixit: Some("add bounds check before operation".into()),
});
}
}
}
diags
}
}
impl StaticChecker for PortabilityChecker {
fn name(&self) -> &str {
"portability"
}
fn description(&self) -> &str {
"Detects non-portable constructs and undefined behavior"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result.diagnostics.extend(self.check_type_sizes(source));
result.diagnostics.extend(self.check_alignment(source));
result.diagnostics.extend(self.check_endianness(source));
result
.diagnostics
.extend(self.check_undefined_behavior(source));
result.files_checked = 1;
result
}
}
impl Default for PortabilityChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SecurityChecker {
pub check_buffer_overflow: bool,
pub check_format_string: bool,
pub check_integer_overflow: bool,
pub check_use_after_free: bool,
}
impl SecurityChecker {
pub fn new() -> Self {
Self {
check_buffer_overflow: true,
check_format_string: true,
check_integer_overflow: true,
check_use_after_free: true,
}
}
pub fn check_buffer_overflow(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let dangerous = ["strcpy", "strcat", "sprintf", "gets", "scanf(\"%s\""];
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
for func in &dangerous {
if line.contains(func) {
diags.push(CheckerDiagnostic {
checker: "security".into(),
rule_id: "buffer_overflow".into(),
severity: DiagLevel::Error,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: format!(
"Use of unsafe function '{}' may cause buffer overflow",
func
),
fixit: Some(format!(
"replace {} with safer alternative (strncpy, snprintf, etc.)",
func
)),
});
}
}
}
diags
}
pub fn check_format_string(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("printf(") && !line.contains("\"") {
diags.push(CheckerDiagnostic {
checker: "security".into(),
rule_id: "format_string".into(),
severity: DiagLevel::Error,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Format string vulnerability: user-controlled format string".into(),
fixit: Some("use printf(\"%s\", user_str) instead of printf(user_str)".into()),
});
}
}
diags
}
pub fn check_integer_overflow(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
if source.contains("malloc") && source.contains("*") && !source.contains("checked") {
diags.push(CheckerDiagnostic {
checker: "security".into(),
rule_id: "integer_overflow".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: 1,
column: 1,
message: "Integer overflow possible in malloc size calculation".into(),
fixit: Some("use calloc or check for overflow before multiplication".into()),
});
}
diags
}
pub fn check_use_after_free(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
let mut freed_vars: HashSet<String> = HashSet::new();
for (i, line) in lines.iter().enumerate() {
if line.contains("free(") {
if let Some(start) = line.find("free(") {
if let Some(end) = line[start + 5..].find(')') {
freed_vars.insert(line[start + 5..start + 5 + end].trim().to_string());
}
}
}
if line.contains("delete ") {
freed_vars.insert("heap_ptr".to_string());
}
}
for var in &freed_vars {
for (i, line) in lines.iter().enumerate() {
if line.contains(var) && !line.contains("free(") && !line.contains("delete ") {
diags.push(CheckerDiagnostic {
checker: "security".into(),
rule_id: "use_after_free".into(),
severity: DiagLevel::Error,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: format!("Use-after-free detected for '{}'", var),
fixit: Some("set pointer to nullptr after free".into()),
});
}
}
}
diags
}
}
impl StaticChecker for SecurityChecker {
fn name(&self) -> &str {
"security"
}
fn description(&self) -> &str {
"Detects buffer overflows, format string bugs, integer overflow, use-after-free"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result
.diagnostics
.extend(self.check_buffer_overflow(source));
result.diagnostics.extend(self.check_format_string(source));
result
.diagnostics
.extend(self.check_integer_overflow(source));
result.diagnostics.extend(self.check_use_after_free(source));
result.files_checked = 1;
result
}
}
impl Default for SecurityChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct StyleChecker {
pub check_naming: bool,
pub check_braces: bool,
pub check_indentation: bool,
pub check_comments: bool,
}
impl StyleChecker {
pub fn new() -> Self {
Self {
check_naming: true,
check_braces: true,
check_indentation: true,
check_comments: true,
}
}
pub fn check_naming(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("void ") || line.contains("int ") || line.contains("auto ") {
if let Some(start) = line.find("void ") {
let rest = &line[start + 5..];
if let Some(name) = rest.split('(').next() {
let name = name.trim();
if name.chars().all(|c| c.is_uppercase() || c == '_') && name.len() > 2 {
diags.push(CheckerDiagnostic {
checker: "style".into(),
rule_id: "naming".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: format!(
"Function '{}' uses ALL_CAPS; prefer snake_case",
name
),
fixit: Some("rename function to snake_case".into()),
});
}
}
}
}
}
diags
}
pub fn check_braces(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if (line.contains("if (") || line.contains("for (") || line.contains("while ("))
&& !line.trim().ends_with('{')
&& i + 1 < lines.len()
{
let next = lines[i + 1].trim();
if next == "{" {
} else if next.starts_with('{') && !line.ends_with('{') {
diags.push(CheckerDiagnostic {
checker: "style".into(),
rule_id: "brace_style".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Inconsistent brace style".into(),
fixit: Some("use consistent brace placement".into()),
});
}
}
}
diags
}
pub fn check_indentation(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
let mut prev_indent = 0;
for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
continue;
}
let indent = line.len() - line.trim_start().len();
if i > 0 && indent > prev_indent + 8 {
diags.push(CheckerDiagnostic {
checker: "style".into(),
rule_id: "indentation".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: indent + 1,
message: "Suspicious indentation (more than 8 spaces increase)".into(),
fixit: Some("use consistent 2 or 4 space indentation".into()),
});
}
prev_indent = indent;
}
diags
}
pub fn check_comments(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("// TODO") || line.trim().starts_with("// FIXME") {
diags.push(CheckerDiagnostic {
checker: "style".into(),
rule_id: "todo_comment".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "TODO/FIXME comment found".into(),
fixit: None,
});
}
}
diags
}
}
impl StaticChecker for StyleChecker {
fn name(&self) -> &str {
"style"
}
fn description(&self) -> &str {
"Checks naming conventions, brace style, indentation, and comments"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result.diagnostics.extend(self.check_naming(source));
result.diagnostics.extend(self.check_braces(source));
result.diagnostics.extend(self.check_indentation(source));
result.diagnostics.extend(self.check_comments(source));
result.files_checked = 1;
result
}
}
impl Default for StyleChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DocumentationChecker {
pub require_public_api_docs: bool,
pub require_params_docs: bool,
}
impl DocumentationChecker {
pub fn new() -> Self {
Self {
require_public_api_docs: true,
require_params_docs: true,
}
}
pub fn check_missing_docs(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
let mut has_doc = false;
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("///")
|| line.trim().starts_with("//!")
|| line.trim().starts_with("/**")
|| line.trim().starts_with("/*!")
{
has_doc = true;
}
if (line.contains("void ") || line.contains("int ") || line.contains("auto "))
&& line.contains('(')
&& line.contains(')')
&& !has_doc
&& !line.contains("private:")
&& !line.contains("static")
{
diags.push(CheckerDiagnostic {
checker: "documentation".into(),
rule_id: "missing_doc".into(),
severity: DiagLevel::Warning,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Missing documentation for public API".into(),
fixit: Some("add /// doc comment before function".into()),
});
}
if line.contains(';') || line.contains('}') {
has_doc = false;
}
}
diags
}
pub fn check_param_docs(&self, source: &str) -> Vec<CheckerDiagnostic> {
let mut diags = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains('(') && line.contains(')') && !line.contains("()") {
if i > 0 && !lines[i - 1].contains("@param") && !lines[i - 1].contains("\\param") {
let doc_above = if i >= 2 {
lines[i - 2].contains("///")
} else {
false
};
if !doc_above {
if line.contains("void ") || line.contains("int ") || line.contains("auto ")
{
diags.push(CheckerDiagnostic {
checker: "documentation".into(),
rule_id: "missing_param_doc".into(),
severity: DiagLevel::Note,
file: "input.cpp".into(),
line: i + 1,
column: 1,
message: "Function has parameters but no @param documentation"
.into(),
fixit: Some("add @param tags for each parameter".into()),
});
}
}
}
}
}
diags
}
}
impl StaticChecker for DocumentationChecker {
fn name(&self) -> &str {
"documentation"
}
fn description(&self) -> &str {
"Checks missing documentation for public APIs and parameters"
}
fn check(&self, source: &str) -> CheckerResult {
let mut result = CheckerResult::new(self.name());
result.diagnostics.extend(self.check_missing_docs(source));
result.diagnostics.extend(self.check_param_docs(source));
result.files_checked = 1;
result
}
}
impl Default for DocumentationChecker {
fn default() -> Self {
Self::new()
}
}
pub struct Checker2Registry {
checkers: Vec<Box<dyn StaticChecker>>,
}
impl Checker2Registry {
pub fn new() -> Self {
let mut registry = Self {
checkers: Vec::new(),
};
registry
.checkers
.push(Box::new(CppCoreGuidelinesChecker::new()));
registry.checkers.push(Box::new(ThreadSafetyChecker::new()));
registry.checkers.push(Box::new(PerformanceAnalyzer::new()));
registry.checkers.push(Box::new(ModernizeChecker::new()));
registry.checkers.push(Box::new(PortabilityChecker::new()));
registry.checkers.push(Box::new(SecurityChecker::new()));
registry.checkers.push(Box::new(StyleChecker::new()));
registry
.checkers
.push(Box::new(DocumentationChecker::new()));
registry
}
pub fn add_checker(&mut self, checker: Box<dyn StaticChecker>) {
self.checkers.push(checker);
}
pub fn run_all(&self, source: &str) -> Vec<CheckerResult> {
self.checkers.iter().map(|c| c.check(source)).collect()
}
pub fn checker_names(&self) -> Vec<&str> {
self.checkers.iter().map(|c| c.name()).collect()
}
pub fn count(&self) -> usize {
self.checkers.len()
}
}
impl Default for Checker2Registry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diag_level_as_str() {
assert_eq!(DiagLevel::Warning.as_str(), "warning");
assert_eq!(DiagLevel::Error.as_str(), "error");
}
#[test]
fn test_cppcoreguidelines_c128_virtual_override() {
let checker = CppCoreGuidelinesChecker::new();
let source = "virtual void foo() override;";
let diags = checker.check_c128(source);
assert!(!diags.is_empty());
}
#[test]
fn test_cppcoreguidelines_es23() {
let checker = CppCoreGuidelinesChecker::new();
let source = "int x = 0;";
let diags = checker.check_es23(source);
assert!(!diags.is_empty());
}
#[test]
fn test_cppcoreguidelines_i3() {
let checker = CppCoreGuidelinesChecker::new();
let source = "static Foo* getInstance();";
let diags = checker.check_i3(source);
assert!(!diags.is_empty());
}
#[test]
fn test_cppcoreguidelines_f6() {
let checker = CppCoreGuidelinesChecker::new();
let source = "// precondition: x > 0";
let diags = checker.check_f6(source);
assert!(!diags.is_empty());
}
#[test]
fn test_cppcoreguidelines_full_check() {
let checker = CppCoreGuidelinesChecker::new();
let source = "virtual void f() override;\nint x = 0;\nstatic Foo* getInstance();";
let result = checker.check(source);
assert!(!result.diagnostics.is_empty());
}
#[test]
fn test_lock_annotation_parsing() {
assert!(matches!(
LockAnnotation::from_attribute("guarded_by(mu)"),
Some(LockAnnotation::GuardedBy(_))
));
assert!(matches!(
LockAnnotation::from_attribute("exclusive_lock_function"),
Some(LockAnnotation::ExclusiveLockFunction)
));
assert!(LockAnnotation::from_attribute("bogus").is_none());
}
#[test]
fn test_thread_safety_double_lock() {
let checker = ThreadSafetyChecker::new();
let source = "mu.lock();\nmu.lock();";
let diags = checker.check_double_lock("mu", source);
assert!(!diags.is_empty());
}
#[test]
fn test_thread_safety_missing_lock() {
let checker = ThreadSafetyChecker::new();
let result = checker.check("std::mutex mu;\nint x = 42;");
assert!(!result.diagnostics.is_empty());
}
#[test]
fn test_performance_unnecessary_copy() {
let analyzer = PerformanceAnalyzer::new();
let source = "auto x = vec[0];";
let diags = analyzer.check_unnecessary_copies(source);
assert!(!diags.is_empty());
}
#[test]
fn test_performance_pass_by_value() {
let analyzer = PerformanceAnalyzer::new();
let source = "void f(const std::string& s)";
let diags = analyzer.check_pass_by_value_sink(source);
assert!(!diags.is_empty());
}
#[test]
fn test_performance_emplace_back() {
let analyzer = PerformanceAnalyzer::new();
let source = "v.push_back(std::make_pair(1, 2));";
let diags = analyzer.check_emplace_vs_push(source);
assert!(!diags.is_empty());
}
#[test]
fn test_modernize_nullptr() {
let checker = ModernizeChecker::new();
let source = "char* p = NULL;";
let diags = checker.check_nullptr(source);
assert!(!diags.is_empty());
}
#[test]
fn test_modernize_override_redundant() {
let checker = ModernizeChecker::new();
let source = "virtual void f() override;";
let diags = checker.check_override(source);
assert!(!diags.is_empty());
}
#[test]
fn test_modernize_auto() {
let checker = ModernizeChecker::new();
let source = "std::vector<int>::iterator it = v.begin();";
let diags = checker.check_auto(source);
assert!(!diags.is_empty());
}
#[test]
fn test_modernize_range_for() {
let checker = ModernizeChecker::new();
let source = "for (auto it = v.begin(); it != v.end(); ++it)";
let diags = checker.check_range_for(source);
assert!(!diags.is_empty());
}
#[test]
fn test_portability_type_sizes() {
let checker = PortabilityChecker::new();
let source = "int n = sizeof(int);";
let diags = checker.check_type_sizes(source);
assert!(!diags.is_empty());
}
#[test]
fn test_portability_alignment() {
let checker = PortabilityChecker::new();
let source = "auto* p = reinterpret_cast<char*>(&x);";
let diags = checker.check_alignment(source);
assert!(!diags.is_empty());
}
#[test]
fn test_portability_undefined_behavior() {
let checker = PortabilityChecker::new();
let source = "int x = a << 32;";
let diags = checker.check_undefined_behavior(source);
assert!(!diags.is_empty());
}
#[test]
fn test_security_buffer_overflow() {
let checker = SecurityChecker::new();
let source = "strcpy(buf, src);";
let diags = checker.check_buffer_overflow(source);
assert!(!diags.is_empty());
}
#[test]
fn test_security_format_string() {
let checker = SecurityChecker::new();
let source = "printf(user_input);";
let diags = checker.check_format_string(source);
assert!(!diags.is_empty());
}
#[test]
fn test_security_integer_overflow() {
let checker = SecurityChecker::new();
let source = "char* p = malloc(n * sizeof(int));";
let diags = checker.check_integer_overflow(source);
assert!(!diags.is_empty());
}
#[test]
fn test_style_naming() {
let checker = StyleChecker::new();
let source = "void MY_FUNCTION() { }";
let diags = checker.check_naming(source);
assert!(!diags.is_empty());
}
#[test]
fn test_style_todo_comment() {
let checker = StyleChecker::new();
let source = "// TODO: fix this";
let diags = checker.check_comments(source);
assert!(!diags.is_empty());
}
#[test]
fn test_doc_missing_public_api() {
let checker = DocumentationChecker::new();
let source = "void doWork() { }";
let diags = checker.check_missing_docs(source);
assert!(!diags.is_empty());
}
#[test]
fn test_doc_missing_params() {
let checker = DocumentationChecker::new();
let source = "void f(int a, int b) { }";
let diags = checker.check_param_docs(source);
assert!(!diags.is_empty());
}
#[test]
fn test_checker2_registry() {
let reg = Checker2Registry::new();
assert_eq!(reg.count(), 8);
let names = reg.checker_names();
assert!(names.contains(&"cppcoreguidelines"));
assert!(names.contains(&"security"));
assert!(names.contains(&"style"));
}
#[test]
fn test_checker2_registry_run_all() {
let reg = Checker2Registry::new();
let source = "virtual void f() override;\nstrcpy(buf, src);\nvoid MY_FUNC() { }";
let results = reg.run_all(source);
assert_eq!(results.len(), 8);
for r in &results {
assert!(
!r.diagnostics.is_empty() || r.files_checked > 0,
"checker {} found nothing",
r.checker_name
);
}
}
}