use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
use super::ast::*;
use super::CLangStandard;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DiagSeverity {
Ignored = 0,
Note = 1,
Warning = 2,
Error = 3,
Fatal = 4,
}
impl fmt::Display for DiagSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagSeverity::Ignored => write!(f, "ignored"),
DiagSeverity::Note => write!(f, "note"),
DiagSeverity::Warning => write!(f, "warning"),
DiagSeverity::Error => write!(f, "error"),
DiagSeverity::Fatal => write!(f, "fatal"),
}
}
}
#[derive(Debug, Clone)]
pub struct SemaDiagnostic {
pub severity: DiagSeverity,
pub message: String,
pub check_name: String,
pub line: Option<usize>,
pub column: Option<usize>,
pub note: Option<String>,
}
impl SemaDiagnostic {
pub fn new(severity: DiagSeverity, check_name: &str, message: &str) -> Self {
Self {
severity,
message: message.to_string(),
check_name: check_name.to_string(),
line: None,
column: None,
note: None,
}
}
pub fn with_location(mut self, line: usize, col: usize) -> Self {
self.line = Some(line);
self.column = Some(col);
self
}
pub fn with_note(mut self, note: &str) -> Self {
self.note = Some(note.to_string());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct DiagCollector {
pub diagnostics: Vec<SemaDiagnostic>,
pub error_count: usize,
pub warning_count: usize,
pub note_count: usize,
}
impl DiagCollector {
pub fn new() -> Self {
Self::default()
}
pub fn emit(&mut self, diag: SemaDiagnostic) {
match diag.severity {
DiagSeverity::Error | DiagSeverity::Fatal => {
self.error_count += 1;
}
DiagSeverity::Warning => {
self.warning_count += 1;
}
DiagSeverity::Note => {
self.note_count += 1;
}
DiagSeverity::Ignored => {}
}
self.diagnostics.push(diag);
}
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
pub fn errors(&self) -> impl Iterator<Item = &SemaDiagnostic> {
self.diagnostics
.iter()
.filter(|d| d.severity >= DiagSeverity::Error)
}
pub fn warnings(&self) -> impl Iterator<Item = &SemaDiagnostic> {
self.diagnostics
.iter()
.filter(|d| d.severity == DiagSeverity::Warning)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompatibilityResult {
Compatible,
CompatibleWithWarning(String),
Incompatible(String),
}
pub fn check_assignment_compatibility(
lhs: &QualType,
rhs: &QualType,
standard: CLangStandard,
) -> CompatibilityResult {
if lhs.base.is_integer() && rhs.base.is_integer() {
return CompatibilityResult::Compatible;
}
if lhs.base.is_integer()
&& matches!(
*rhs.base,
TypeNode::Float | TypeNode::Double | TypeNode::LongDouble
)
{
return CompatibilityResult::Compatible;
}
if matches!(
*lhs.base,
TypeNode::Float | TypeNode::Double | TypeNode::LongDouble
) && rhs.base.is_integer()
{
return CompatibilityResult::Compatible;
}
if matches!(
*lhs.base,
TypeNode::Float | TypeNode::Double | TypeNode::LongDouble
) && matches!(
*rhs.base,
TypeNode::Float | TypeNode::Double | TypeNode::LongDouble
) {
return CompatibilityResult::Compatible;
}
if let (TypeNode::Pointer(plhs), TypeNode::Pointer(prhs)) = (&*lhs.base, &*rhs.base) {
if matches!(*plhs.base, TypeNode::Void) || matches!(*prhs.base, TypeNode::Void) {
return CompatibilityResult::Compatible;
}
if plhs.is_const == prhs.is_const && plhs.is_volatile == prhs.is_volatile {
return CompatibilityResult::Compatible;
}
if prhs.is_const && !plhs.is_const {
return CompatibilityResult::CompatibleWithWarning(
"discarding 'const' qualifier on pointer target".into(),
);
}
}
if let (TypeNode::Struct { name: na, .. }, TypeNode::Struct { name: nb, .. }) =
(&*lhs.base, &*rhs.base)
{
if na == nb {
return CompatibilityResult::Compatible;
} else {
return CompatibilityResult::Incompatible(format!(
"incompatible struct types: {:?} vs {:?}",
na, nb
));
}
}
if lhs.base == rhs.base {
return CompatibilityResult::Compatible;
}
CompatibilityResult::Incompatible(format!("cannot assign '{}' to '{}'", rhs, lhs))
}
pub fn check_integer_conversion_loss(from: &TypeNode, to: &TypeNode) -> Option<String> {
let from_size = from.size_bytes();
let to_size = to.size_bytes();
if from_size > to_size {
return Some(format!(
"implicit conversion from '{}' to '{}' may lose precision",
from.kind_name(),
to.kind_name()
));
}
if from_size == to_size && from.is_signed_integer() && to.is_unsigned() {
return Some(format!(
"implicit sign conversion from '{}' to '{}'",
from.kind_name(),
to.kind_name()
));
}
None
}
pub fn check_sign_comparison(left: &TypeNode, right: &TypeNode) -> Option<String> {
if left.is_unsigned() && right.is_signed_integer() {
return Some(format!(
"comparison between signed '{}' and unsigned '{}'",
right.kind_name(),
left.kind_name()
));
}
if left.is_signed_integer() && right.is_unsigned() {
return Some(format!(
"comparison between signed '{}' and unsigned '{}'",
left.kind_name(),
right.kind_name()
));
}
None
}
pub fn integer_rank(ty: &TypeNode) -> Option<u32> {
match ty {
TypeNode::Bool => Some(0),
TypeNode::Char | TypeNode::SChar | TypeNode::UChar => Some(1),
TypeNode::Short | TypeNode::UShort => Some(2),
TypeNode::Int | TypeNode::UInt => Some(3),
TypeNode::Long | TypeNode::ULong => Some(4),
TypeNode::LongLong | TypeNode::ULongLong => Some(5),
_ => None,
}
}
pub fn check_conversion_rank(from: &TypeNode, to: &TypeNode) -> CompatibilityResult {
let rank_from = integer_rank(from);
let rank_to = integer_rank(to);
match (rank_from, rank_to) {
(Some(rf), Some(rt)) => {
if rf > rt {
CompatibilityResult::CompatibleWithWarning(format!(
"implicit conversion loses integer rank: '{}' ({}) to '{}' ({})",
from.kind_name(),
rf,
to.kind_name(),
rt
))
} else {
CompatibilityResult::Compatible
}
}
_ => CompatibilityResult::Compatible,
}
}
pub fn check_const_correctness(target_ty: &QualType, operation: &str) -> Option<String> {
if target_ty.is_const {
Some(format!(
"{} of read-only (const-qualified) type '{}'",
operation, target_ty
))
} else {
None
}
}
pub fn check_volatile_access(target_ty: &QualType, operation: &str) -> Option<String> {
if target_ty.is_volatile {
Some(format!(
"{} of volatile-qualified type '{}'",
operation, target_ty
))
} else {
None
}
}
pub fn check_restrict_aliasing(ptr1_ty: &QualType, ptr2_ty: &QualType) -> Option<String> {
if ptr1_ty.is_restrict && ptr2_ty.is_restrict {
if let (TypeNode::Pointer(inner1), TypeNode::Pointer(inner2)) =
(&*ptr1_ty.base, &*ptr2_ty.base)
{
if inner1.base == inner2.base {
return Some(
"potential aliasing violation: two restrict-qualified pointers to the same type"
.into(),
);
}
}
}
None
}
pub fn check_qualifier_discard(lhs: &QualType, rhs: &QualType) -> Vec<String> {
let mut warnings = Vec::new();
if rhs.is_const && !lhs.is_const {
warnings.push("discarding 'const' qualifier".into());
}
if rhs.is_volatile && !lhs.is_volatile {
warnings.push("discarding 'volatile' qualifier".into());
}
if rhs.is_restrict && !lhs.is_restrict {
warnings.push("discarding 'restrict' qualifier".into());
}
warnings
}
fn get_function_type_node(ty: &QualType) -> Option<&TypeNode> {
match &*ty.base {
TypeNode::Function { .. } => Some(&*ty.base),
TypeNode::Pointer(inner) => match &*inner.base {
TypeNode::Function { .. } => Some(&*inner.base),
_ => None,
},
_ => None,
}
}
pub fn check_function_pointer_compatibility(
fptr_a: &QualType,
fptr_b: &QualType,
) -> CompatibilityResult {
let fn_type_a = get_function_type_node(fptr_a);
let fn_type_b = get_function_type_node(fptr_b);
let (fn_a, fn_b) = match (fn_type_a, fn_type_b) {
(Some(a), Some(b)) => (a, b),
_ => {
return CompatibilityResult::Incompatible("not function pointer types".into());
}
};
if let (
TypeNode::Function {
ret: ra,
params: pa,
is_vararg: va,
},
TypeNode::Function {
ret: rb,
params: pb,
is_vararg: vb,
},
) = (fn_a, fn_b)
{
if va != vb {
return CompatibilityResult::Incompatible("vararg flag mismatch".into());
}
if ra.base != rb.base {
return CompatibilityResult::Incompatible(format!(
"return type mismatch: '{}' vs '{}'",
ra, rb
));
}
if pa.len() != pb.len() {
return CompatibilityResult::Incompatible(format!(
"parameter count mismatch: {} vs {}",
pa.len(),
pb.len()
));
}
for (a, b) in pa.iter().zip(pb.iter()) {
if a.base != b.base {
return CompatibilityResult::Incompatible(format!(
"parameter type mismatch: '{}' vs '{}'",
a, b
));
}
}
CompatibilityResult::Compatible
} else {
CompatibilityResult::Incompatible("not function types".into())
}
}
pub fn adjust_parameter_type(param_ty: &QualType) -> QualType {
match &*param_ty.base {
TypeNode::Array { elem, .. } => QualType {
base: Box::new(TypeNode::Pointer(elem.clone())),
is_const: param_ty.is_const,
is_volatile: param_ty.is_volatile,
is_restrict: param_ty.is_restrict,
},
TypeNode::Function { .. } => QualType {
base: Box::new(TypeNode::Pointer(Box::new(param_ty.clone()))),
is_const: param_ty.is_const,
is_volatile: param_ty.is_volatile,
is_restrict: param_ty.is_restrict,
},
_ => param_ty.clone(),
}
}
pub fn check_array_parameter_compatibility(
param: &QualType,
arg: &QualType,
) -> CompatibilityResult {
let adjusted = adjust_parameter_type(param);
if adjusted.base == arg.base {
CompatibilityResult::Compatible
} else {
CompatibilityResult::Incompatible(format!(
"array/function parameter type mismatch: expected '{}', got '{}'",
adjusted, arg
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefinitionState {
DeclaredOnly,
Tentative,
Defined,
}
pub fn classify_variable_definition(var: &VarDecl) -> DefinitionState {
if !var.is_global {
return DefinitionState::Defined;
}
if var.is_extern && var.init.is_none() {
return DefinitionState::DeclaredOnly;
}
if var.init.is_some() {
return DefinitionState::Defined;
}
DefinitionState::Tentative
}
pub fn resolve_tentative_definitions(declarations: &[Decl]) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut var_states: HashMap<String, Vec<(DefinitionState, usize)>> = HashMap::new();
for (idx, decl) in declarations.iter().enumerate() {
if let Decl::Variable(var) = decl {
let state = classify_variable_definition(var);
var_states
.entry(var.name.clone())
.or_default()
.push((state, idx));
}
}
for (name, states) in &var_states {
let definition_count = states
.iter()
.filter(|(s, _)| *s == DefinitionState::Defined)
.count();
if definition_count > 1 {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"tentative-definition",
&format!("multiple definitions of '{}'", name),
));
}
let tentative_count = states
.iter()
.filter(|(s, _)| *s == DefinitionState::Tentative)
.count();
if definition_count == 0 && tentative_count > 1 {
diags.push(SemaDiagnostic::new(
DiagSeverity::Note,
"tentative-definition",
&format!(
"multiple tentative definitions of '{}' — will be merged",
name
),
));
}
}
diags
}
pub fn detect_duplicate_declarations(declarations: &[Decl]) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut seen: HashMap<String, &Decl> = HashMap::new();
for decl in declarations {
if let Some(name) = decl.name() {
if let Some(prev) = seen.get(name) {
match (decl, *prev) {
(Decl::Function(fa), Decl::Function(fb)) => {
if fa.ret_ty != fb.ret_ty
|| fa.params.len() != fb.params.len()
|| fa.is_vararg != fb.is_vararg
{
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"duplicate-decl",
&format!("conflicting types for '{}'", name),
));
}
}
(Decl::Variable(va), Decl::Variable(vb)) => {
if va.ty != vb.ty {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"duplicate-decl",
&format!("conflicting types for '{}'", name),
));
}
}
_ => {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"duplicate-decl",
&format!("redefinition of '{}' as a different kind of symbol", name),
));
}
}
}
seen.insert(name.to_string(), decl);
}
}
diags
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ODRViolation {
MultipleFunctionDefinitions { name: String, locations: Vec<usize> },
MultipleVariableDefinitions { name: String, locations: Vec<usize> },
InlineDefinitionMismatch { name: String, details: String },
TypeDefinitionMismatch { name: String, details: String },
}
pub fn detect_odr_violations(declarations: &[Decl]) -> Vec<ODRViolation> {
let mut violations = Vec::new();
let mut fun_defs: HashMap<String, Vec<usize>> = HashMap::new();
let mut var_defs: HashMap<String, Vec<usize>> = HashMap::new();
let mut struct_defs: HashMap<String, Vec<(usize, StructDecl)>> = HashMap::new();
let mut enum_defs: HashMap<String, Vec<(usize, EnumDecl)>> = HashMap::new();
for (idx, decl) in declarations.iter().enumerate() {
match decl {
Decl::Function(f) => {
if f.body.is_some() {
fun_defs.entry(f.name.clone()).or_default().push(idx);
}
}
Decl::Variable(v) => {
if v.init.is_some() && v.is_global {
var_defs.entry(v.name.clone()).or_default().push(idx);
}
}
Decl::Struct(s) => {
if let Some(ref name) = s.name {
struct_defs
.entry(name.clone())
.or_default()
.push((idx, s.clone()));
}
}
Decl::Enum(e) => {
if let Some(ref name) = e.name {
enum_defs
.entry(name.clone())
.or_default()
.push((idx, e.clone()));
}
}
_ => {}
}
}
for (name, locs) in &fun_defs {
if locs.len() > 1 {
violations.push(ODRViolation::MultipleFunctionDefinitions {
name: name.clone(),
locations: locs.clone(),
});
}
}
for (name, locs) in &var_defs {
if locs.len() > 1 {
violations.push(ODRViolation::MultipleVariableDefinitions {
name: name.clone(),
locations: locs.clone(),
});
}
}
for (name, defs) in &struct_defs {
if defs.len() > 1 {
let first = &defs[0].1;
for (_, other) in &defs[1..] {
if first.fields.len() != other.fields.len() || first.is_union != other.is_union {
violations.push(ODRViolation::TypeDefinitionMismatch {
name: name.clone(),
details: "struct definition differs".into(),
});
break;
}
}
}
}
for (name, defs) in &enum_defs {
if defs.len() > 1 {
let first = &defs[0].1;
for (_, other) in &defs[1..] {
if first.variants.len() != other.variants.len() {
violations.push(ODRViolation::TypeDefinitionMismatch {
name: name.clone(),
details: "enum definition differs".into(),
});
break;
}
}
}
}
violations
}
pub fn check_unreachable_code(stmts: &[Stmt]) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut prev_terminates = false;
for (idx, stmt) in stmts.iter().enumerate() {
if prev_terminates {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"unreachable-code",
&format!("statement at position {} is unreachable", idx),
));
break; }
prev_terminates = does_statement_terminate(stmt);
}
diags
}
fn does_statement_terminate(stmt: &Stmt) -> bool {
match stmt {
Stmt::Return(_) | Stmt::Break | Stmt::Continue => true,
Stmt::Goto { .. } => true,
Stmt::Compound(c) => c
.stmts
.last()
.map(does_statement_terminate)
.unwrap_or(false),
Stmt::If { then, els, .. } => {
does_statement_terminate(then)
&& els
.as_ref()
.map(|e| does_statement_terminate(e))
.unwrap_or(false)
}
Stmt::Switch { body, .. } => {
all_cases_terminate(body)
}
Stmt::Null | Stmt::Expr(_) | Stmt::Decl(_) | Stmt::Label { .. } => false,
_ => false,
}
}
fn all_cases_terminate(body: &Stmt) -> bool {
if let Stmt::Compound(c) = body {
let mut has_default = false;
let mut all_terminate = true;
for stmt in &c.stmts {
match stmt {
Stmt::Case {
stmt: case_body, ..
} => {
if !does_statement_terminate(case_body) {
all_terminate = false;
}
}
Stmt::Default { stmt: default_body } => {
has_default = true;
if !does_statement_terminate(default_body) {
all_terminate = false;
}
}
_ => {}
}
}
return has_default && all_terminate;
}
false
}
pub fn check_return_type(func: &FunctionDecl) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let ret_is_void = func.ret_ty.is_void();
if func.is_noreturn {
if let Some(ref body) = func.body {
if has_return_statement(body) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"noreturn-return",
&format!(
"function '{}' declared '_Noreturn' has a return statement",
func.name
),
));
}
}
return diags;
}
if let Some(ref body) = func.body {
let stmt = Stmt::Compound(body.clone());
check_return_statements(&stmt, &func.ret_ty, ret_is_void, &func.name, &mut diags);
}
if !ret_is_void && !func.is_noreturn {
if let Some(ref body) = func.body {
if !all_paths_return(body) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"return-type",
&format!("control reaches end of non-void function '{}'", func.name),
));
}
}
}
diags
}
fn check_return_statements(
stmt: &Stmt,
ret_ty: &QualType,
ret_is_void: bool,
func_name: &str,
diags: &mut Vec<SemaDiagnostic>,
) {
match stmt {
Stmt::Return(Some(expr)) => {
if ret_is_void {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"return-type",
&format!("void function '{}' should not return a value", func_name),
));
}
}
Stmt::Return(None) => {
if !ret_is_void {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"return-type",
&format!("non-void function '{}' should return a value", func_name),
));
}
}
Stmt::Compound(c) => {
for s in &c.stmts {
check_return_statements(s, ret_ty, ret_is_void, func_name, diags);
}
}
Stmt::If { then, els, .. } => {
check_return_statements(then, ret_ty, ret_is_void, func_name, diags);
if let Some(e) = els {
check_return_statements(e, ret_ty, ret_is_void, func_name, diags);
}
}
Stmt::While { body, .. } | Stmt::For { body, .. } => {
check_return_statements(body, ret_ty, ret_is_void, func_name, diags);
}
Stmt::DoWhile { body, .. } => {
check_return_statements(body, ret_ty, ret_is_void, func_name, diags);
}
Stmt::Switch { body, .. } => {
check_return_statements(body, ret_ty, ret_is_void, func_name, diags);
}
Stmt::Case {
stmt: case_body, ..
}
| Stmt::Default { stmt: case_body } => {
check_return_statements(case_body, ret_ty, ret_is_void, func_name, diags);
}
Stmt::Label {
stmt: label_body, ..
} => {
check_return_statements(label_body, ret_ty, ret_is_void, func_name, diags);
}
_ => {}
}
}
fn has_return_statement(stmt: &CompoundStmt) -> bool {
stmt.stmts.iter().any(|s| has_return_recursive(s))
}
fn has_return_recursive(stmt: &Stmt) -> bool {
match stmt {
Stmt::Return(_) => true,
Stmt::Compound(c) => c.stmts.iter().any(|s| has_return_recursive(s)),
Stmt::If { then, els, .. } => {
has_return_recursive(then)
|| els
.as_ref()
.map(|e| has_return_recursive(e))
.unwrap_or(false)
}
Stmt::While { body, .. }
| Stmt::For { body, .. }
| Stmt::DoWhile { body, .. }
| Stmt::Switch { body, .. } => has_return_recursive(body),
Stmt::Case {
stmt: case_body, ..
}
| Stmt::Default { stmt: case_body }
| Stmt::Label {
stmt: case_body, ..
} => has_return_recursive(case_body),
_ => false,
}
}
fn all_paths_return(stmt: &CompoundStmt) -> bool {
if stmt.stmts.is_empty() {
return false;
}
let last = stmt.stmts.last().unwrap();
does_statement_terminate(last)
}
pub fn check_fallthrough(switch_body: &CompoundStmt) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut prev_case_terminates = true;
for stmt in &switch_body.stmts {
match stmt {
Stmt::Case {
stmt: case_body, ..
} => {
if !prev_case_terminates {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"implicit-fallthrough",
"unannotated fallthrough between switch cases",
));
}
prev_case_terminates = does_statement_terminate(case_body);
}
Stmt::Default { stmt: default_body } => {
if !prev_case_terminates {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"implicit-fallthrough",
"unannotated fallthrough into default case",
));
}
prev_case_terminates = does_statement_terminate(default_body);
}
Stmt::Break => {
prev_case_terminates = true;
}
Stmt::Return(_) => {
prev_case_terminates = true;
}
_ => {
}
}
}
diags
}
pub fn detect_uninitialized_variables(func: &FunctionDecl) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut initialized: HashSet<String> = HashSet::new();
for param in &func.params {
initialized.insert(param.name.clone());
}
if let Some(ref body) = func.body {
check_uninit_in_compound(body, &mut initialized, &mut diags);
}
diags
}
fn check_uninit_in_compound(
stmt: &CompoundStmt,
initialized: &mut HashSet<String>,
diags: &mut Vec<SemaDiagnostic>,
) {
for s in &stmt.stmts {
check_uninit_in_stmt(s, initialized, diags);
}
}
fn check_uninit_in_stmt(
stmt: &Stmt,
initialized: &mut HashSet<String>,
diags: &mut Vec<SemaDiagnostic>,
) {
match stmt {
Stmt::Decl(v) => {
if v.init.is_some() {
initialized.insert(v.name.clone());
}
}
Stmt::Expr(e) => {
check_uninit_in_expr(e, initialized, diags);
}
Stmt::Compound(c) => {
let mut scope_inited = initialized.clone();
check_uninit_in_compound(c, &mut scope_inited, diags);
}
Stmt::If { cond, then, els } => {
check_uninit_in_expr(cond, initialized, diags);
let mut then_inited = initialized.clone();
check_uninit_in_stmt(then, &mut then_inited, diags);
if let Some(e) = els {
let mut else_inited = initialized.clone();
check_uninit_in_stmt(e, &mut else_inited, diags);
let both_inited: HashSet<_> =
then_inited.intersection(&else_inited).cloned().collect();
*initialized = both_inited;
}
}
Stmt::While { cond, body } | Stmt::DoWhile { cond, body } => {
check_uninit_in_expr(cond, initialized, diags);
check_uninit_in_stmt(body, initialized, diags);
}
Stmt::For {
init,
cond,
incr,
body,
} => {
if let Some(i) = init {
check_uninit_in_stmt(i, initialized, diags);
}
if let Some(c) = cond {
check_uninit_in_expr(c, initialized, diags);
}
check_uninit_in_stmt(body, initialized, diags);
if let Some(i) = incr {
check_uninit_in_expr(i, initialized, diags);
}
}
Stmt::Switch { expr, body } => {
check_uninit_in_expr(expr, initialized, diags);
check_uninit_in_stmt(body, initialized, diags);
}
Stmt::Return(Some(e)) => {
check_uninit_in_expr(e, initialized, diags);
}
Stmt::Case {
stmt: case_body, ..
}
| Stmt::Default { stmt: case_body }
| Stmt::Label {
stmt: case_body, ..
} => {
check_uninit_in_stmt(case_body, initialized, diags);
}
_ => {}
}
}
fn check_uninit_in_expr(
expr: &Expr,
initialized: &HashSet<String>,
diags: &mut Vec<SemaDiagnostic>,
) {
match expr {
Expr::Ident(name) => {
if !initialized.contains(name.as_str()) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"uninitialized",
&format!("variable '{}' may be used uninitialized", name),
));
}
}
Expr::Binary(_, lhs, rhs) => {
check_uninit_in_expr(lhs, initialized, diags);
check_uninit_in_expr(rhs, initialized, diags);
}
Expr::Unary(_, inner) => {
check_uninit_in_expr(inner, initialized, diags);
}
Expr::Assign(_, lhs, rhs) => {
check_uninit_in_expr(lhs, initialized, diags);
check_uninit_in_expr(rhs, initialized, diags);
}
Expr::Call { callee, args } => {
check_uninit_in_expr(callee, initialized, diags);
for arg in args {
check_uninit_in_expr(arg, initialized, diags);
}
}
Expr::Conditional(cond, then, els) => {
check_uninit_in_expr(cond, initialized, diags);
check_uninit_in_expr(then, initialized, diags);
check_uninit_in_expr(els, initialized, diags);
}
Expr::Cast(_, inner) => {
check_uninit_in_expr(inner, initialized, diags);
}
Expr::Subscript { base, index } => {
check_uninit_in_expr(base, initialized, diags);
check_uninit_in_expr(index, initialized, diags);
}
Expr::Member { base, .. } => {
check_uninit_in_expr(base, initialized, diags);
}
Expr::PostInc(inner) | Expr::PostDec(inner) | Expr::PreInc(inner) | Expr::PreDec(inner) => {
check_uninit_in_expr(inner, initialized, diags);
}
Expr::SizeOf(inner) | Expr::AlignOf(inner) => {
check_uninit_in_expr(inner, initialized, diags);
}
_ => {}
}
}
pub fn detect_self_assignment(stmt: &Stmt) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
detect_self_assign_in_stmt(stmt, &mut diags);
diags
}
fn detect_self_assign_in_stmt(stmt: &Stmt, diags: &mut Vec<SemaDiagnostic>) {
match stmt {
Stmt::Expr(e) => detect_self_assign_in_expr(e, diags),
Stmt::Compound(c) => {
for s in &c.stmts {
detect_self_assign_in_stmt(s, diags);
}
}
Stmt::If { cond, then, els } => {
detect_self_assign_in_expr(cond, diags);
detect_self_assign_in_stmt(then, diags);
if let Some(e) = els {
detect_self_assign_in_stmt(e, diags);
}
}
Stmt::While { cond, body } | Stmt::DoWhile { cond, body } => {
detect_self_assign_in_expr(cond, diags);
detect_self_assign_in_stmt(body, diags);
}
Stmt::For {
init,
cond,
incr,
body,
} => {
if let Some(i) = init {
detect_self_assign_in_stmt(i, diags);
}
if let Some(c) = cond {
detect_self_assign_in_expr(c, diags);
}
if let Some(i) = incr {
detect_self_assign_in_expr(i, diags);
}
detect_self_assign_in_stmt(body, diags);
}
Stmt::Switch { expr, body } => {
detect_self_assign_in_expr(expr, diags);
detect_self_assign_in_stmt(body, diags);
}
_ => {}
}
}
fn detect_self_assign_in_expr(expr: &Expr, diags: &mut Vec<SemaDiagnostic>) {
match expr {
Expr::Assign(_, lhs, rhs) => {
if expr_text_equals(lhs, rhs) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"self-assign",
"explicit self-assignment detected",
));
}
detect_self_assign_in_expr(lhs, diags);
detect_self_assign_in_expr(rhs, diags);
}
Expr::Binary(_, lhs, rhs) => {
detect_self_assign_in_expr(lhs, diags);
detect_self_assign_in_expr(rhs, diags);
}
Expr::Unary(_, inner) => detect_self_assign_in_expr(inner, diags),
Expr::Cast(_, inner) => detect_self_assign_in_expr(inner, diags),
Expr::Call { callee, args } => {
detect_self_assign_in_expr(callee, diags);
for arg in args {
detect_self_assign_in_expr(arg, diags);
}
}
_ => {}
}
}
fn expr_text_equals(a: &Expr, b: &Expr) -> bool {
format!("{}", a) == format!("{}", b)
}
pub fn is_null_pointer_constant(expr: &Expr) -> bool {
match expr {
Expr::IntLiteral(0) | Expr::UIntLiteral(0, _) => true,
Expr::Cast(ty, inner) if matches!(*ty.base, TypeNode::Pointer(_)) => {
is_null_pointer_constant(inner)
}
Expr::SizeOf(_) | Expr::SizeOfType(_) => false,
_ => false,
}
}
pub fn check_pointer_integer_conversion(
expr_ty: &QualType,
expected_ty: &QualType,
context: &str,
) -> Option<String> {
let is_ptr = matches!(*expr_ty.base, TypeNode::Pointer(_));
let is_int = expr_ty.base.is_integer();
let exp_ptr = matches!(*expected_ty.base, TypeNode::Pointer(_));
let exp_int = expected_ty.base.is_integer();
if (is_ptr && exp_int) || (is_int && exp_ptr) {
Some(format!(
"{} between pointer ('{}') and integer ('{}') without a cast",
context, expr_ty, expected_ty
))
} else {
None
}
}
pub fn is_integer_constant_expression(expr: &Expr) -> bool {
match expr {
Expr::IntLiteral(_)
| Expr::UIntLiteral(..)
| Expr::CharLiteral(_)
| Expr::SizeOf(_)
| Expr::SizeOfType(_)
| Expr::AlignOf(_)
| Expr::AlignOfType(_) => true,
Expr::Cast(_, inner) => is_integer_constant_expression(inner),
Expr::Unary(_, inner) => is_integer_constant_expression(inner),
Expr::Binary(_, lhs, rhs) => {
is_integer_constant_expression(lhs) && is_integer_constant_expression(rhs)
}
Expr::Conditional(cond, then, els) => {
is_integer_constant_expression(cond)
&& is_integer_constant_expression(then)
&& is_integer_constant_expression(els)
}
_ => false,
}
}
pub fn evaluate_integer_constant_expression(expr: &Expr) -> Option<i64> {
match expr {
Expr::IntLiteral(v) => Some(*v),
Expr::UIntLiteral(v, _) => Some(*v as i64),
Expr::CharLiteral(c) => Some(*c as i64),
Expr::Cast(_, inner) => evaluate_integer_constant_expression(inner),
Expr::Unary(UnaryOp::Plus, inner) => evaluate_integer_constant_expression(inner),
Expr::Unary(UnaryOp::Minus, inner) => {
evaluate_integer_constant_expression(inner).map(|v| -v)
}
Expr::Unary(UnaryOp::BitNot, inner) => {
evaluate_integer_constant_expression(inner).map(|v| !v)
}
Expr::Binary(BinaryOp::Add, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l.wrapping_add(r))
}
Expr::Binary(BinaryOp::Sub, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l.wrapping_sub(r))
}
Expr::Binary(BinaryOp::Mul, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l.wrapping_mul(r))
}
Expr::Binary(BinaryOp::Div, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
if r == 0 {
None
} else {
Some(l.wrapping_div(r))
}
}
Expr::Binary(BinaryOp::Mod, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
if r == 0 {
None
} else {
Some(l.wrapping_rem(r))
}
}
Expr::Conditional(cond, then, els) => {
let c = evaluate_integer_constant_expression(cond)?;
if c != 0 {
evaluate_integer_constant_expression(then)
} else {
evaluate_integer_constant_expression(els)
}
}
Expr::Binary(BinaryOp::Shl, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l.wrapping_shl(r as u32))
}
Expr::Binary(BinaryOp::Shr, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l.wrapping_shr(r as u32))
}
Expr::Binary(BinaryOp::And, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l & r)
}
Expr::Binary(BinaryOp::Or, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l | r)
}
Expr::Binary(BinaryOp::Xor, lhs, rhs) => {
let l = evaluate_integer_constant_expression(lhs)?;
let r = evaluate_integer_constant_expression(rhs)?;
Some(l ^ r)
}
_ => None,
}
}
pub fn check_flexible_array_member_position(struct_decl: &StructDecl) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
if struct_decl.fields.len() <= 1 {
return diags;
}
for (idx, field) in struct_decl.fields.iter().enumerate() {
if let TypeNode::Array { size: None, .. } = &*field.ty.base {
if idx != struct_decl.fields.len() - 1 {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"flexible-array-member",
&format!(
"flexible array member '{}' must be the last member of '{}'",
field.name,
struct_decl.name.as_deref().unwrap_or("(anonymous)")
),
));
}
break;
}
}
diags
}
pub fn check_main_signature(func: &FunctionDecl) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
if func.name != "main" {
return diags;
}
if !matches!(*func.ret_ty.base, TypeNode::Int) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"main-return-type",
"return type of 'main' is not 'int'",
));
}
match func.params.len() {
0 => {
}
1 => {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"main-signature",
"'main' takes only one argument — expected 'int, char**'",
));
}
2 => {
if !matches!(*func.params[0].ty.base, TypeNode::Int) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"main-signature",
"first parameter of 'main' (argc) must be 'int'",
));
}
let is_char_ptr_ptr = match &*func.params[1].ty.base {
TypeNode::Pointer(inner) => match &*inner.base {
TypeNode::Pointer(inner2) => {
matches!(*inner2.base, TypeNode::Char)
}
_ => false,
},
_ => false,
};
if !is_char_ptr_ptr {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"main-signature",
"second parameter of 'main' (argv) must be 'char**' or equivalent",
));
}
}
_ => {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"main-signature",
"'main' takes too many parameters",
));
}
}
diags
}
pub fn check_vla_size(size_expr: &Expr, array_name: &str) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
if let Some(value) = evaluate_integer_constant_expression(size_expr) {
if value <= 0 {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"vla-size",
&format!(
"VLA '{}' size must be greater than zero (got {})",
array_name, value
),
));
}
if value > 65536 {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"vla-size",
&format!(
"VLA '{}' size is very large ({}) — may cause stack overflow",
array_name, value
),
));
}
}
diags
}
pub fn check_c_cast_validity(target_ty: &QualType, source_ty: &QualType) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
if matches!(*source_ty.base, TypeNode::Pointer(_))
&& source_ty.base.is_integer()
&& matches!(
*target_ty.base,
TypeNode::Int
| TypeNode::Long
| TypeNode::LongLong
| TypeNode::UInt
| TypeNode::ULong
| TypeNode::ULongLong
)
{
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"cast-align",
"cast from pointer to integer of different size",
));
}
if target_ty.base.is_integer()
&& matches!(
*source_ty.base,
TypeNode::Int
| TypeNode::Long
| TypeNode::LongLong
| TypeNode::UInt
| TypeNode::ULong
| TypeNode::ULongLong
)
&& matches!(*target_ty.base, TypeNode::Pointer(_))
{
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"int-to-pointer-cast",
"cast to pointer from integer of different size",
));
}
if let (TypeNode::Pointer(inner_src), TypeNode::Pointer(inner_dst)) =
(&*source_ty.base, &*target_ty.base)
{
if !matches!(*inner_src.base, TypeNode::Void)
&& !matches!(*inner_dst.base, TypeNode::Void)
&& inner_src.base != inner_dst.base
{
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"incompatible-pointer-types",
&format!(
"incompatible pointer types: '{}' to '{}'",
source_ty, target_ty
),
));
}
}
diags
}
pub fn check_const_cast(target_ty: &QualType, source_ty: &QualType) -> Option<String> {
if source_ty.is_const && !target_ty.is_const {
if let (TypeNode::Pointer(_), TypeNode::Pointer(_)) = (&*source_ty.base, &*target_ty.base) {
return Some(format!(
"cast from '{}' to '{}' discards const qualifier",
source_ty, target_ty
));
}
}
None
}
pub fn check_reinterpret_cast_safety(
target_ty: &QualType,
source_ty: &QualType,
) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let src_is_fn_ptr = matches!(&*source_ty.base, TypeNode::Pointer(inner)
if matches!(&*inner.base, TypeNode::Function { .. }));
let dst_is_fn_ptr = matches!(&*target_ty.base, TypeNode::Pointer(inner)
if matches!(&*inner.base, TypeNode::Function { .. }));
let src_is_data_ptr = matches!(&*source_ty.base, TypeNode::Pointer(inner)
if !matches!(&*inner.base, TypeNode::Function { .. }));
let dst_is_data_ptr = matches!(&*target_ty.base, TypeNode::Pointer(inner)
if !matches!(&*inner.base, TypeNode::Function { .. }));
if (src_is_fn_ptr && dst_is_data_ptr) || (src_is_data_ptr && dst_is_fn_ptr) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"bad-function-cast",
"cast between function pointer and data pointer",
));
}
diags
}
#[derive(Debug, Clone)]
pub struct FormatSpecifier {
pub specifier: char,
pub position: usize,
pub long_mod: bool,
pub longlong_mod: bool,
}
pub fn parse_format_specifiers(format_str: &str) -> Vec<FormatSpecifier> {
let mut specs = Vec::new();
let chars: Vec<char> = format_str.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' {
i += 1;
if i >= chars.len() {
break;
}
while i < chars.len() && matches!(chars[i], '-' | '+' | ' ' | '#' | '0') {
i += 1;
}
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
if i < chars.len() && chars[i] == '.' {
i += 1;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
}
let mut long_mod = false;
let mut longlong_mod = false;
if i < chars.len() && chars[i] == 'l' {
long_mod = true;
i += 1;
if i < chars.len() && chars[i] == 'l' {
longlong_mod = true;
i += 1;
}
}
if i < chars.len() && chars[i] == 'h' {
i += 1;
if i < chars.len() && chars[i] == 'h' {
i += 1;
}
}
if i < chars.len() {
let spec = chars[i];
if matches!(
spec,
'd' | 'i'
| 'u'
| 'o'
| 'x'
| 'X'
| 'f'
| 'e'
| 'E'
| 'g'
| 'G'
| 's'
| 'c'
| 'p'
| 'n'
| '%'
) {
specs.push(FormatSpecifier {
specifier: spec,
position: i,
long_mod,
longlong_mod,
});
}
}
}
i += 1;
}
specs
}
fn count_format_args(specs: &[FormatSpecifier]) -> usize {
specs.iter().filter(|s| s.specifier != '%').count()
}
pub fn check_format_string(format_str: &str, actual_arg_count: usize) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let specs = parse_format_specifiers(format_str);
let expected_args = count_format_args(&specs);
if expected_args > actual_arg_count {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"format",
&format!(
"format string expects {} arguments but {} provided",
expected_args, actual_arg_count
),
));
} else if expected_args < actual_arg_count {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"format-extra-args",
&format!(
"{} extra arguments provided to format string",
actual_arg_count - expected_args
),
));
}
for spec in &specs {
match spec.specifier {
'd' | 'i' | 'u' | 'o' | 'x' | 'X' | 'f' | 'e' | 'E' | 'g' | 'G' | 's' | 'c' | 'p'
| 'n' | '%' => {}
_ => {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"format-invalid-specifier",
&format!(
"invalid format specifier '{}' at position {}",
spec.specifier, spec.position
),
));
}
}
}
diags
}
pub struct X86SemaChecks {
pub standard: CLangStandard,
pub diagnostics: DiagCollector,
pub wall: bool,
pub wextra: bool,
pub werror: bool,
pub pedantic: bool,
pub format_checks: bool,
pub unreachable_code_checks: bool,
pub sign_compare_checks: bool,
pub self_assign_checks: bool,
pub uninit_checks: bool,
pub fallthrough_checks: bool,
pub cast_checks: bool,
pub odr_checks: bool,
pub checks_run: usize,
}
impl X86SemaChecks {
pub fn new(standard: CLangStandard) -> Self {
Self {
standard,
diagnostics: DiagCollector::new(),
wall: false,
wextra: false,
werror: false,
pedantic: false,
format_checks: false,
unreachable_code_checks: false,
sign_compare_checks: false,
self_assign_checks: false,
uninit_checks: false,
fallthrough_checks: false,
cast_checks: false,
odr_checks: false,
checks_run: 0,
}
}
pub fn enable_wall(&mut self) {
self.wall = true;
self.unreachable_code_checks = true;
self.sign_compare_checks = true;
self.self_assign_checks = true;
self.uninit_checks = true;
self.fallthrough_checks = true;
self.format_checks = true;
self.cast_checks = true;
}
pub fn enable_wextra(&mut self) {
self.wextra = true;
self.sign_compare_checks = true;
self.cast_checks = true;
}
pub fn check_all(&mut self, declarations: &[Decl]) {
self.checks_run = 0;
self.run_check("duplicate-declarations", || {
detect_duplicate_declarations(declarations)
});
self.run_check("tentative-definitions", || {
resolve_tentative_definitions(declarations)
});
for decl in declarations {
if let Decl::Function(f) = decl {
self.run_check("main-signature", || check_main_signature(f));
}
}
for decl in declarations {
if let Decl::Struct(s) = decl {
self.run_check("flexible-array-member", || {
check_flexible_array_member_position(s)
});
}
}
if self.odr_checks {
self.run_check("odr-violations", || {
let violations = detect_odr_violations(declarations);
let mut diags = Vec::new();
for v in &violations {
let msg = match v {
ODRViolation::MultipleFunctionDefinitions { name, .. } => {
format!("ODR violation: multiple definitions of function '{}'", name)
}
ODRViolation::MultipleVariableDefinitions { name, .. } => {
format!("ODR violation: multiple definitions of variable '{}'", name)
}
ODRViolation::InlineDefinitionMismatch { name, details } => {
format!(
"ODR violation: inline definition mismatch for '{}': {}",
name, details
)
}
ODRViolation::TypeDefinitionMismatch { name, details } => {
format!(
"ODR violation: type definition mismatch for '{}': {}",
name, details
)
}
};
diags.push(SemaDiagnostic::new(DiagSeverity::Error, "odr", &msg));
}
diags
});
}
for decl in declarations {
if let Decl::Function(f) = decl {
self.run_check("return-type", || check_return_type(f));
if self.uninit_checks {
self.run_check("uninitialized", || detect_uninitialized_variables(f));
}
if self.self_assign_checks {
if let Some(ref body) = f.body {
let stmt = Stmt::Compound(body.clone());
self.run_check("self-assign", || detect_self_assignment(&stmt));
}
if self.fallthrough_checks {
if let Some(ref body) = f.body {
for stmt in &body.stmts {
if let Stmt::Switch {
body: switch_body, ..
} = stmt
{
if let Stmt::Compound(c) = &**switch_body {
self.run_check("fallthrough", || check_fallthrough(c));
}
}
}
}
}
if self.unreachable_code_checks {
if let Some(ref body) = f.body {
self.run_check("unreachable-code", || {
check_unreachable_code(&body.stmts)
});
}
}
}
}
}
self.run_check("null-pointer-constant", || {
let mut diags = Vec::new();
diags
});
self.run_check("vla-size", || {
let mut diags = Vec::new();
diags
});
}
fn run_check<F>(&mut self, _name: &str, f: F)
where
F: FnOnce() -> Vec<SemaDiagnostic>,
{
self.checks_run += 1;
let diags = f();
for diag in diags {
let effective_severity = if self.werror && diag.severity == DiagSeverity::Warning {
DiagSeverity::Error
} else {
diag.severity
};
self.diagnostics.emit(SemaDiagnostic {
severity: effective_severity,
..diag
});
}
}
pub fn check_assign(&mut self, lhs: &QualType, rhs: &QualType) -> CompatibilityResult {
self.checks_run += 1;
check_assignment_compatibility(lhs, rhs, self.standard)
}
pub fn check_const(&mut self, target_ty: &QualType, operation: &str) -> Option<String> {
self.checks_run += 1;
check_const_correctness(target_ty, operation)
}
pub fn check_fn_ptr(&mut self, fptr_a: &QualType, fptr_b: &QualType) -> CompatibilityResult {
self.checks_run += 1;
check_function_pointer_compatibility(fptr_a, fptr_b)
}
pub fn check_cast(
&mut self,
target_ty: &QualType,
source_ty: &QualType,
) -> Vec<SemaDiagnostic> {
self.checks_run += 1;
let mut diags = check_c_cast_validity(target_ty, source_ty);
if let Some(msg) = check_const_cast(target_ty, source_ty) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"const-cast",
&msg,
));
}
if self.cast_checks {
diags.extend(check_reinterpret_cast_safety(target_ty, source_ty));
}
diags
}
pub fn check_format(&mut self, format_str: &str, arg_count: usize) -> Vec<SemaDiagnostic> {
self.checks_run += 1;
if self.format_checks {
check_format_string(format_str, arg_count)
} else {
Vec::new()
}
}
pub fn check_sign_compare(&mut self, left: &TypeNode, right: &TypeNode) -> Option<String> {
self.checks_run += 1;
if self.sign_compare_checks {
check_sign_comparison(left, right)
} else {
None
}
}
pub fn check_ice(&mut self, expr: &Expr) -> bool {
self.checks_run += 1;
is_integer_constant_expression(expr)
}
pub fn check_null_ptr_const(&mut self, expr: &Expr) -> bool {
self.checks_run += 1;
is_null_pointer_constant(expr)
}
pub fn eval_ice(&mut self, expr: &Expr) -> Option<i64> {
self.checks_run += 1;
evaluate_integer_constant_expression(expr)
}
pub fn report(&self) -> String {
let mut s = String::new();
s.push_str("Sema Deep Checks Report\n");
s.push_str("=======================\n");
s.push_str(&format!("Checks run: {}\n", self.checks_run));
s.push_str(&format!(
"Errors: {}\n",
self.diagnostics.error_count
));
s.push_str(&format!(
"Warnings: {}\n",
self.diagnostics.warning_count
));
s.push_str(&format!(
"Notes: {}\n",
self.diagnostics.note_count
));
s.push_str(&format!(
"Total diags: {}\n",
self.diagnostics.diagnostics.len()
));
s.push_str(&format!("-Wall: {}\n", self.wall));
s.push_str(&format!("-Wextra: {}\n", self.wextra));
s.push_str(&format!("-Werror: {}\n", self.werror));
if self.diagnostics.has_errors() {
s.push_str("\nErrors:\n");
for diag in self.diagnostics.errors() {
s.push_str(&format!(
" [{}] {}: {}\n",
diag.severity, diag.check_name, diag.message
));
}
}
if self.diagnostics.warning_count > 0 {
s.push_str("\nWarnings:\n");
for diag in self.diagnostics.warnings() {
s.push_str(&format!(
" [{}] {}: {}\n",
diag.severity, diag.check_name, diag.message
));
}
}
s
}
}
pub fn check_incomplete_parameter_type(param_ty: &QualType, param_name: &str) -> Option<String> {
match &*param_ty.base {
TypeNode::Void => Some(format!(
"parameter '{}' has incomplete type 'void'",
param_name
)),
TypeNode::Array { size: None, .. } => Some(format!(
"parameter '{}' has incomplete array type",
param_name
)),
_ => None,
}
}
pub fn check_duplicate_fields(struct_decl: &StructDecl) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut seen = HashSet::new();
for field in &struct_decl.fields {
if !seen.insert(&field.name) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"duplicate-member",
&format!(
"duplicate member '{}' in '{}'",
field.name,
struct_decl.name.as_deref().unwrap_or("(anonymous)")
),
));
}
}
diags
}
pub fn check_duplicate_enumerators(enum_decl: &EnumDecl) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut seen = HashSet::new();
for variant in &enum_decl.variants {
if !seen.insert(&variant.name) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"duplicate-enumerator",
&format!(
"duplicate enumerator '{}' in '{}'",
variant.name,
enum_decl.name.as_deref().unwrap_or("(anonymous)")
),
));
}
}
diags
}
pub fn check_duplicate_cases(switch_body: &CompoundStmt) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut case_values: HashMap<i64, usize> = HashMap::new();
for (idx, stmt) in switch_body.stmts.iter().enumerate() {
if let Stmt::Case { value, .. } = stmt {
if let Some(val) = evaluate_integer_constant_expression(value) {
if let Some(&prev_idx) = case_values.get(&val) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Error,
"duplicate-case",
&format!(
"duplicate case value {} (previously used at position {})",
val, prev_idx
),
));
} else {
case_values.insert(val, idx);
}
}
}
}
diags
}
pub fn check_empty_body(stmt: &Stmt, context: &str) -> Option<String> {
match stmt {
Stmt::Compound(c) if c.is_empty() => {
Some(format!("empty body in {} — possible logic error", context))
}
Stmt::Null => Some(format!(
"null statement as {} — possible logic error",
context
)),
_ => None,
}
}
pub fn check_unused_labels(body: &CompoundStmt) -> Vec<SemaDiagnostic> {
let mut diags = Vec::new();
let mut defined_labels = HashSet::new();
let mut used_labels = HashSet::new();
collect_labels_in_compound(body, &mut defined_labels, &mut used_labels);
for label in &defined_labels {
if !used_labels.contains(label) {
diags.push(SemaDiagnostic::new(
DiagSeverity::Warning,
"unused-label",
&format!("label '{}' is defined but not used", label),
));
}
}
diags
}
fn collect_labels_in_compound(
stmt: &CompoundStmt,
defined: &mut HashSet<String>,
used: &mut HashSet<String>,
) {
for s in &stmt.stmts {
collect_labels_in_stmt(s, defined, used);
}
}
fn collect_labels_in_stmt(stmt: &Stmt, defined: &mut HashSet<String>, used: &mut HashSet<String>) {
match stmt {
Stmt::Label { name, stmt: inner } => {
defined.insert(name.clone());
collect_labels_in_stmt(inner, defined, used);
}
Stmt::Goto { label } => {
used.insert(label.clone());
}
Stmt::Compound(c) => collect_labels_in_compound(c, defined, used),
Stmt::If { then, els, .. } => {
collect_labels_in_stmt(then, defined, used);
if let Some(e) = els {
collect_labels_in_stmt(e, defined, used);
}
}
Stmt::While { body, .. }
| Stmt::DoWhile { body, .. }
| Stmt::For { body, .. }
| Stmt::Switch { body, .. } => {
collect_labels_in_stmt(body, defined, used);
}
Stmt::Case { stmt: inner, .. } | Stmt::Default { stmt: inner } => {
collect_labels_in_stmt(inner, defined, used);
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c11_std() -> CLangStandard {
CLangStandard::C11
}
fn make_int() -> QualType {
QualType::int()
}
fn make_void() -> QualType {
QualType::void()
}
fn make_char_ptr() -> QualType {
QualType::pointer_to(QualType::char())
}
fn make_const_int() -> QualType {
QualType::const_(TypeNode::Int)
}
#[test]
fn test_assign_compat_int_to_int() {
let result = check_assignment_compatibility(&make_int(), &make_int(), c11_std());
assert_eq!(result, CompatibilityResult::Compatible);
}
#[test]
fn test_assign_compat_void_ptr() {
let void_ptr = QualType::pointer_to(QualType::void());
let char_ptr = make_char_ptr();
let result = check_assignment_compatibility(&void_ptr, &char_ptr, c11_std());
assert_eq!(result, CompatibilityResult::Compatible);
}
#[test]
fn test_assign_compat_incompatible() {
let int_ty = make_int();
let result = check_assignment_compatibility(&make_void(), &int_ty, c11_std());
assert!(matches!(result, CompatibilityResult::Incompatible(_)));
}
#[test]
fn test_sign_compare_mixed() {
let result = check_sign_comparison(&TypeNode::UInt, &TypeNode::Int);
assert!(result.is_some());
}
#[test]
fn test_sign_compare_same_signed() {
let result = check_sign_comparison(&TypeNode::Int, &TypeNode::Long);
assert!(result.is_none());
}
#[test]
fn test_const_correctness_modify_const() {
let result = check_const_correctness(&make_const_int(), "modification");
assert!(result.is_some());
}
#[test]
fn test_const_correctness_modify_nonconst() {
let result = check_const_correctness(&make_int(), "modification");
assert!(result.is_none());
}
#[test]
fn test_fn_ptr_compat_same() {
let fn_ty = QualType::new(TypeNode::Function {
ret: Box::new(make_int()),
params: vec![make_int()],
is_vararg: false,
});
let result = check_function_pointer_compatibility(&fn_ty, &fn_ty);
assert_eq!(result, CompatibilityResult::Compatible);
}
#[test]
fn test_fn_ptr_compat_different_params() {
let fn_a = QualType::new(TypeNode::Function {
ret: Box::new(make_int()),
params: vec![make_int()],
is_vararg: false,
});
let fn_b = QualType::new(TypeNode::Function {
ret: Box::new(make_int()),
params: vec![make_int(), make_int()],
is_vararg: false,
});
let result = check_function_pointer_compatibility(&fn_a, &fn_b);
assert!(matches!(result, CompatibilityResult::Incompatible(_)));
}
#[test]
fn test_return_type_void_no_return_value() {
let func = FunctionDecl {
name: "test".into(),
ret_ty: make_void(),
params: vec![],
body: Some(CompoundStmt::new()),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = check_return_type(&func);
}
#[test]
fn test_return_type_nonvoid_no_return() {
let func = FunctionDecl {
name: "test".into(),
ret_ty: make_int(),
params: vec![],
body: Some(CompoundStmt::new()),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = check_return_type(&func);
assert!(!diags.is_empty());
assert!(diags.iter().any(|d| d.check_name == "return-type"));
}
#[test]
fn test_main_good_signature() {
let main_func = FunctionDecl {
name: "main".into(),
ret_ty: make_int(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = check_main_signature(&main_func);
assert!(diags.is_empty());
}
#[test]
fn test_main_bad_return_type() {
let main_func = FunctionDecl {
name: "main".into(),
ret_ty: make_void(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = check_main_signature(&main_func);
assert!(diags.iter().any(|d| d.check_name == "main-return-type"));
}
#[test]
fn test_not_main_ignored() {
let func = FunctionDecl {
name: "not_main".into(),
ret_ty: make_void(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = check_main_signature(&func);
assert!(diags.is_empty());
}
#[test]
fn test_ice_simple() {
assert!(is_integer_constant_expression(&Expr::IntLiteral(42)));
assert!(is_integer_constant_expression(&Expr::CharLiteral('A')));
assert!(!is_integer_constant_expression(&Expr::Ident("x".into())));
}
#[test]
fn test_ice_evaluate() {
let expr = Expr::Binary(
BinaryOp::Add,
Box::new(Expr::IntLiteral(3)),
Box::new(Expr::IntLiteral(4)),
);
assert_eq!(evaluate_integer_constant_expression(&expr), Some(7));
}
#[test]
fn test_ice_evaluate_complex() {
let expr = Expr::Binary(
BinaryOp::Mul,
Box::new(Expr::Binary(
BinaryOp::Add,
Box::new(Expr::IntLiteral(10)),
Box::new(Expr::IntLiteral(2)),
)),
Box::new(Expr::IntLiteral(3)),
);
assert_eq!(evaluate_integer_constant_expression(&expr), Some(36));
}
#[test]
fn test_ice_division_by_zero() {
let expr = Expr::Binary(
BinaryOp::Div,
Box::new(Expr::IntLiteral(5)),
Box::new(Expr::IntLiteral(0)),
);
assert_eq!(evaluate_integer_constant_expression(&expr), None);
}
#[test]
fn test_null_ptr_const_zero() {
assert!(is_null_pointer_constant(&Expr::IntLiteral(0)));
}
#[test]
fn test_null_ptr_const_nonzero() {
assert!(!is_null_pointer_constant(&Expr::IntLiteral(1)));
}
#[test]
fn test_null_ptr_const_cast_zero() {
let expr = Expr::Cast(
QualType::pointer_to(QualType::void()),
Box::new(Expr::IntLiteral(0)),
);
assert!(is_null_pointer_constant(&expr));
}
#[test]
fn test_duplicate_decl_no_dupes() {
let decls = vec![
Decl::Function(FunctionDecl {
name: "foo".into(),
ret_ty: make_int(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
}),
Decl::Variable(VarDecl::new("bar", make_int())),
];
let diags = detect_duplicate_declarations(&decls);
assert!(diags.is_empty());
}
#[test]
fn test_duplicate_decl_conflict() {
let decls = vec![
Decl::Function(FunctionDecl {
name: "conflict".into(),
ret_ty: make_int(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
}),
Decl::Function(FunctionDecl {
name: "conflict".into(),
ret_ty: make_void(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
}),
];
let diags = detect_duplicate_declarations(&decls);
assert!(!diags.is_empty());
}
#[test]
fn test_fam_valid() {
let s = StructDecl {
name: Some("S".into()),
fields: vec![
FieldDecl::new("n", make_int()),
FieldDecl {
name: "data".into(),
ty: QualType::new(TypeNode::Array {
elem: Box::new(QualType::char()),
size: None,
}),
bit_width: None,
},
],
is_union: false,
};
let diags = check_flexible_array_member_position(&s);
assert!(diags.is_empty());
}
#[test]
fn test_fam_not_last() {
let s = StructDecl {
name: Some("S".into()),
fields: vec![
FieldDecl {
name: "data".into(),
ty: QualType::new(TypeNode::Array {
elem: Box::new(QualType::char()),
size: None,
}),
bit_width: None,
},
FieldDecl::new("n", make_int()),
],
is_union: false,
};
let diags = check_flexible_array_member_position(&s);
assert!(!diags.is_empty());
}
#[test]
fn test_fallthrough_detected() {
let body = CompoundStmt {
stmts: vec![
Stmt::Case {
value: Box::new(Expr::IntLiteral(1)),
stmt: Box::new(Stmt::Expr(Box::new(Expr::Ident("x".into())))),
},
Stmt::Case {
value: Box::new(Expr::IntLiteral(2)),
stmt: Box::new(Stmt::Break),
},
],
};
let diags = check_fallthrough(&body);
assert!(!diags.is_empty());
}
#[test]
fn test_fallthrough_clean() {
let body = CompoundStmt {
stmts: vec![
Stmt::Case {
value: Box::new(Expr::IntLiteral(1)),
stmt: Box::new(Stmt::Break),
},
Stmt::Case {
value: Box::new(Expr::IntLiteral(2)),
stmt: Box::new(Stmt::Break),
},
],
};
let diags = check_fallthrough(&body);
assert!(diags.is_empty());
}
#[test]
fn test_unreachable_after_return() {
let stmts = vec![
Stmt::Return(Some(Box::new(Expr::IntLiteral(0)))),
Stmt::Expr(Box::new(Expr::Ident("dead".into()))),
];
let diags = check_unreachable_code(&stmts);
assert!(!diags.is_empty());
}
#[test]
fn test_no_unreachable() {
let stmts = vec![
Stmt::Expr(Box::new(Expr::Ident("a".into()))),
Stmt::Return(Some(Box::new(Expr::IntLiteral(0)))),
];
let diags = check_unreachable_code(&stmts);
assert!(diags.is_empty());
}
#[test]
fn test_uninit_detected() {
let func = FunctionDecl {
name: "test".into(),
ret_ty: make_int(),
params: vec![],
body: Some(CompoundStmt {
stmts: vec![
Stmt::Decl(Box::new(VarDecl::new("x", make_int()))),
Stmt::Expr(Box::new(Expr::Ident("x".into()))),
],
}),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = detect_uninitialized_variables(&func);
assert!(!diags.is_empty());
}
#[test]
fn test_uninit_param_is_init() {
let func = FunctionDecl {
name: "test".into(),
ret_ty: make_int(),
params: vec![VarDecl::new("p", make_int())],
body: Some(CompoundStmt {
stmts: vec![Stmt::Expr(Box::new(Expr::Ident("p".into())))],
}),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let diags = detect_uninitialized_variables(&func);
assert!(diags.is_empty());
}
#[test]
fn test_self_assign_detected() {
let stmt = Stmt::Expr(Box::new(Expr::Assign(
BinaryOp::Assign,
Box::new(Expr::Ident("x".into())),
Box::new(Expr::Ident("x".into())),
)));
let diags = detect_self_assignment(&stmt);
assert!(!diags.is_empty());
}
#[test]
fn test_no_self_assign() {
let stmt = Stmt::Expr(Box::new(Expr::Assign(
BinaryOp::Assign,
Box::new(Expr::Ident("x".into())),
Box::new(Expr::Ident("y".into())),
)));
let diags = detect_self_assignment(&stmt);
assert!(diags.is_empty());
}
#[test]
fn test_cast_const_discard() {
let const_char_ptr = QualType::const_(TypeNode::Pointer(Box::new(QualType::char())));
let char_ptr = make_char_ptr();
let result = check_const_cast(&char_ptr, &const_char_ptr);
assert!(result.is_some());
}
#[test]
fn test_cast_no_const_discard() {
let char_ptr_a = make_char_ptr();
let char_ptr_b = make_char_ptr();
let result = check_const_cast(&char_ptr_a, &char_ptr_b);
assert!(result.is_none());
}
#[test]
fn test_format_parse_specifiers() {
let specs = parse_format_specifiers("hello %d world %s");
assert_eq!(specs.len(), 2);
assert_eq!(specs[0].specifier, 'd');
assert_eq!(specs[1].specifier, 's');
}
#[test]
fn test_format_arg_count_match() {
let diags = check_format_string("x = %d, y = %d", 2);
assert!(diags.is_empty());
}
#[test]
fn test_format_arg_count_mismatch() {
let diags = check_format_string("x = %d, y = %d", 1);
assert!(!diags.is_empty());
}
#[test]
fn test_sema_checks_engine_basic() {
let mut engine = X86SemaChecks::new(c11_std());
engine.enable_wall();
let decls = vec![Decl::Function(FunctionDecl {
name: "test".into(),
ret_ty: make_int(),
params: vec![],
body: Some(CompoundStmt::new()),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
})];
engine.check_all(&decls);
assert!(engine.checks_run > 0);
assert!(!engine.diagnostics.has_errors());
let report = engine.report();
assert!(report.contains("Checks run:"));
}
#[test]
fn test_sema_checks_assign() {
let mut engine = X86SemaChecks::new(c11_std());
let result = engine.check_assign(&make_int(), &make_int());
assert_eq!(result, CompatibilityResult::Compatible);
assert_eq!(engine.checks_run, 1);
}
#[test]
fn test_sema_checks_const() {
let mut engine = X86SemaChecks::new(c11_std());
let result = engine.check_const(&make_const_int(), "assignment");
assert!(result.is_some());
}
#[test]
fn test_sema_checks_cast() {
let mut engine = X86SemaChecks::new(c11_std());
engine.enable_wall();
let diags = engine.check_cast(&make_char_ptr(), &make_char_ptr());
assert!(diags.is_empty());
}
#[test]
fn test_sema_checks_format() {
let mut engine = X86SemaChecks::new(c11_std());
engine.enable_wall();
let diags = engine.check_format("%d %d", 1);
assert!(!diags.is_empty());
}
#[test]
fn test_sema_checks_sign_compare() {
let mut engine = X86SemaChecks::new(c11_std());
engine.enable_wall();
let result = engine.check_sign_compare(&TypeNode::UInt, &TypeNode::Int);
assert!(result.is_some());
}
#[test]
fn test_sema_checks_ice() {
let mut engine = X86SemaChecks::new(c11_std());
assert!(engine.check_ice(&Expr::IntLiteral(42)));
assert!(!engine.check_ice(&Expr::Ident("x".into())));
}
#[test]
fn test_sema_checks_null_ptr() {
let mut engine = X86SemaChecks::new(c11_std());
assert!(engine.check_null_ptr_const(&Expr::IntLiteral(0)));
assert!(!engine.check_null_ptr_const(&Expr::IntLiteral(1)));
}
#[test]
fn test_sema_checks_eval_ice() {
let mut engine = X86SemaChecks::new(c11_std());
let expr = Expr::Binary(
BinaryOp::Mul,
Box::new(Expr::IntLiteral(6)),
Box::new(Expr::IntLiteral(7)),
);
assert_eq!(engine.eval_ice(&expr), Some(42));
}
#[test]
fn test_duplicate_fields() {
let s = StructDecl {
name: Some("Dup".into()),
fields: vec![
FieldDecl::new("x", make_int()),
FieldDecl::new("x", make_int()),
],
is_union: false,
};
let diags = check_duplicate_fields(&s);
assert!(!diags.is_empty());
}
#[test]
fn test_duplicate_enumerators() {
let e = EnumDecl {
name: Some("DupEnum".into()),
variants: vec![
EnumVariant::new("A", Some(0)),
EnumVariant::new("A", Some(1)),
],
underlying: make_int(),
};
let diags = check_duplicate_enumerators(&e);
assert!(!diags.is_empty());
}
#[test]
fn test_duplicate_cases() {
let body = CompoundStmt {
stmts: vec![
Stmt::Case {
value: Box::new(Expr::IntLiteral(1)),
stmt: Box::new(Stmt::Break),
},
Stmt::Case {
value: Box::new(Expr::IntLiteral(1)),
stmt: Box::new(Stmt::Break),
},
],
};
let diags = check_duplicate_cases(&body);
assert!(!diags.is_empty());
}
#[test]
fn test_empty_body_check() {
let empty = Stmt::Compound(CompoundStmt::new());
let result = check_empty_body(&empty, "if statement");
assert!(result.is_some());
}
#[test]
fn test_unused_label() {
let body = CompoundStmt {
stmts: vec![Stmt::Label {
name: "unused".into(),
stmt: Box::new(Stmt::Null),
}],
};
let diags = check_unused_labels(&body);
assert!(!diags.is_empty());
}
#[test]
fn test_used_label_no_warning() {
let body = CompoundStmt {
stmts: vec![
Stmt::Label {
name: "target".into(),
stmt: Box::new(Stmt::Null),
},
Stmt::Goto {
label: "target".into(),
},
],
};
let diags = check_unused_labels(&body);
assert!(diags.is_empty());
}
#[test]
fn test_odr_detection_functions() {
let decls = vec![
Decl::Function(FunctionDecl {
name: "odr_func".into(),
ret_ty: make_int(),
params: vec![],
body: Some(CompoundStmt::new()),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
}),
Decl::Function(FunctionDecl {
name: "odr_func".into(),
ret_ty: make_int(),
params: vec![],
body: Some(CompoundStmt::new()),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
}),
];
let violations = detect_odr_violations(&decls);
assert!(!violations.is_empty());
}
#[test]
fn test_array_parameter_adjustment() {
let arr_param = QualType::new(TypeNode::Array {
elem: Box::new(QualType::int()),
size: Some(10),
});
let adjusted = adjust_parameter_type(&arr_param);
assert!(matches!(*adjusted.base, TypeNode::Pointer(_)));
}
#[test]
fn test_function_parameter_adjustment() {
let fn_param = QualType::new(TypeNode::Function {
ret: Box::new(make_int()),
params: vec![],
is_vararg: false,
});
let adjusted = adjust_parameter_type(&fn_param);
assert!(matches!(*adjusted.base, TypeNode::Pointer(_)));
}
#[test]
fn test_incomplete_parameter_type() {
let void_param = make_void();
let result = check_incomplete_parameter_type(&void_param, "p");
assert!(result.is_some());
}
#[test]
fn test_volatile_access() {
let vol_int = QualType {
base: Box::new(TypeNode::Int),
is_const: false,
is_volatile: true,
is_restrict: false,
};
let result = check_volatile_access(&vol_int, "read");
assert!(result.is_some());
}
#[test]
fn test_qualifier_discard() {
let const_vol = QualType {
base: Box::new(TypeNode::Int),
is_const: true,
is_volatile: true,
is_restrict: false,
};
let plain = make_int();
let warnings = check_qualifier_discard(&plain, &const_vol);
assert_eq!(warnings.len(), 2);
}
#[test]
fn test_sema_checks_report_no_errors() {
let engine = X86SemaChecks::new(c11_std());
let report = engine.report();
assert!(report.contains("Errors: 0"));
assert!(report.contains("Warnings: 0"));
}
#[test]
fn test_sema_checks_werror() {
let mut engine = X86SemaChecks::new(c11_std());
engine.werror = true;
let diag = SemaDiagnostic::new(DiagSeverity::Warning, "test-check", "test warning");
engine.diagnostics.emit(diag);
assert!(engine.diagnostics.has_errors());
}
}