use gobject_ast::model::{BinaryOp, Expression, FileModel, FunctionDefItem, SourceLocation};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct UseGStrHasPrefixSuffix;
impl Rule for UseGStrHasPrefixSuffix {
fn name(&self) -> &'static str {
"use_g_str_has_prefix_suffix"
}
fn description(&self) -> &'static str {
"Use g_str_has_prefix/g_str_has_suffix() instead of manual strncmp/strcmp comparisons"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Style
}
fn fixable(&self) -> bool {
true
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
config: &Config,
func: &FunctionDefItem,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
for stmt in &func.body_statements {
stmt.walk_expressions(&mut |expr| {
expr.walk(&mut |e| {
self.check_expression(e, file, &config.style, violations);
});
});
}
}
}
impl UseGStrHasPrefixSuffix {
fn check_expression(
&self,
expr: &Expression,
file: &FileModel,
style: &crate::config::Style,
violations: &mut Vec<Violation>,
) {
let Expression::Binary(bin) = expr else {
return;
};
if !matches!(bin.operator, BinaryOp::Equal | BinaryOp::NotEqual) {
return;
}
self.check_for_prefix_pattern(
&bin.left,
&bin.right,
&bin.operator,
file,
style,
&bin.location,
violations,
);
self.check_for_prefix_pattern(
&bin.right,
&bin.left,
&bin.operator,
file,
style,
&bin.location,
violations,
);
self.check_for_suffix_pattern(
&bin.left,
&bin.right,
&bin.operator,
file,
style,
&bin.location,
violations,
);
self.check_for_suffix_pattern(
&bin.right,
&bin.left,
&bin.operator,
file,
style,
&bin.location,
violations,
);
}
#[allow(clippy::too_many_arguments)]
fn check_for_prefix_pattern(
&self,
strncmp_side: &Expression,
value_side: &Expression,
operator: &BinaryOp,
file: &FileModel,
style: &crate::config::Style,
location: &SourceLocation,
violations: &mut Vec<Violation>,
) {
let Expression::Call(call) = strncmp_side else {
return;
};
if !call.is_function("strncmp") {
return;
}
if !value_side.is_zero() {
return;
}
if call.arguments.len() != 3 {
return;
}
let Some(prefix_text) = call.arguments[1].extract_string_value() else {
return;
};
if !self.is_strlen_of(&call.arguments[2], &prefix_text) {
return;
}
let str_arg_text = call
.get_arg(0)
.and_then(|e| e.location().as_str())
.unwrap_or_default();
let prefix_arg = format!("\"{}\"", prefix_text);
let call = style.format_call("g_str_has_prefix", &[str_arg_text, &prefix_arg]);
let replacement = if *operator == BinaryOp::Equal {
call
} else {
format!("!{call}")
};
let message = format!(
"Use {replacement} instead of strncmp() {} 0",
operator.as_str()
);
let fix = Fix::new(location.start_byte, location.end_byte, replacement);
violations.push(self.violation_with_fix_at(&file.path, location, message, fix));
}
#[allow(clippy::too_many_arguments)]
fn check_for_suffix_pattern(
&self,
strcmp_side: &Expression,
value_side: &Expression,
operator: &BinaryOp,
file: &FileModel,
style: &crate::config::Style,
location: &SourceLocation,
violations: &mut Vec<Violation>,
) {
let Expression::Call(call) = strcmp_side else {
return;
};
if !call.is_function("strcmp") {
return;
}
if !value_side.is_zero() {
return;
}
if call.arguments.len() != 2 {
return;
}
let Some(suffix_text) = call.arguments[1].extract_string_value() else {
return;
};
let Some(str_expr) = self.extract_suffix_base(&call.arguments[0], &suffix_text) else {
return;
};
let suffix_arg = format!("\"{}\"", suffix_text);
let call = style.format_call("g_str_has_suffix", &[str_expr, &suffix_arg]);
let replacement = if *operator == BinaryOp::Equal {
call
} else {
format!("!{call}")
};
let message = format!(
"Use {replacement} instead of strcmp() {} 0",
operator.as_str()
);
let fix = Fix::new(location.start_byte, location.end_byte, replacement);
violations.push(self.violation_with_fix_at(&file.path, location, message, fix));
}
fn extract_suffix_base<'a>(&self, arg: &'a Expression, suffix_text: &str) -> Option<&'a str> {
let Expression::Binary(top_bin) = arg else {
return None;
};
if top_bin.operator != BinaryOp::Subtract {
return None;
}
if !self.is_strlen_of_arg_by_value(&top_bin.right, suffix_text) {
return None;
}
let Expression::Binary(inner_bin) = &*top_bin.left else {
return None;
};
if inner_bin.operator != BinaryOp::Add {
return None;
}
let str_expr = inner_bin.left.location().as_str()?;
if !self.is_strlen_of_arg(&inner_bin.right, str_expr) {
return None;
}
Some(str_expr)
}
fn is_strlen_of(&self, arg: &Expression, expected_text: &str) -> bool {
let Expression::Call(call) = arg else {
return false;
};
if !call.is_function("strlen") {
return false;
}
if call.arguments.len() != 1 {
return false;
}
if let Some(str_val) = call.arguments[0].extract_string_value() {
return str_val == expected_text;
}
false
}
fn is_strlen_of_arg(&self, expr: &Expression, expected_text_with_quotes: &str) -> bool {
let Expression::Call(call) = expr else {
return false;
};
if !call.is_function("strlen") {
return false;
}
if call.arguments.len() != 1 {
return false;
}
call.get_arg(0).is_some_and(|e| {
e.location()
.as_str()
.is_some_and(|s| s == expected_text_with_quotes)
})
}
fn is_strlen_of_arg_by_value(&self, expr: &Expression, expected_string_value: &str) -> bool {
let Expression::Call(call) = expr else {
return false;
};
if !call.is_function("strlen") {
return false;
}
if call.arguments.len() != 1 {
return false;
}
if let Some(str_val) = call.arguments[0].extract_string_value() {
return str_val == expected_string_value;
}
false
}
}