use std::collections::HashMap;
use gobject_ast::Expression;
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct UseGObjectNotifyByPspec;
impl Rule for UseGObjectNotifyByPspec {
fn name(&self) -> &'static str {
"use_g_object_notify_by_pspec"
}
fn description(&self) -> &'static str {
"Suggest g_object_notify_by_pspec instead of g_object_notify for better performance"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Perf
}
fn fixable(&self) -> bool {
true
}
fn check_all(
&self,
ast_context: &AstContext,
_config: &Config,
violations: &mut Vec<Violation>,
) {
for (path, file) in ast_context.iter_all_files() {
let source = &file.source;
let property_map = self.build_property_map(file);
for func in file.iter_function_definitions() {
for call in func.find_calls(&["g_object_notify"]) {
self.check_call(path, call, source, &property_map, func, violations);
}
}
}
}
}
impl UseGObjectNotifyByPspec {
fn check_call(
&self,
file_path: &std::path::Path,
call: &gobject_ast::CallExpression,
source: &[u8],
property_map: &HashMap<&str, Vec<(&str, &str, &str)>>,
func: &gobject_ast::top_level::FunctionDefItem,
violations: &mut Vec<Violation>,
) {
if call.arguments.len() != 2 {
return;
}
let Some(property_expr) = call.get_arg(1) else {
return;
};
if !property_expr.is_string_literal() {
return;
}
let Expression::StringLiteral(string_lit) = property_expr else {
unreachable!();
};
let property_name = string_lit.value.trim_matches('"');
let array_names: Vec<&str> = property_map
.values()
.flatten()
.map(|(_, array_name, _)| *array_name)
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
if let Some(candidates) = property_map.get(property_name) {
let disambiguated = if candidates.len() > 1 {
self.disambiguate_by_type(call, source, func, candidates)
} else {
Some(&candidates[0])
};
if let Some((enum_value, array_name, _)) = disambiguated {
let Some(obj_expr) = call.get_arg(0) else {
return;
};
let Some(obj_str) = obj_expr.to_source_string(source) else {
return;
};
let replacement = format!(
"g_object_notify_by_pspec ({}, {}[{}])",
obj_str, array_name, enum_value
);
violations.push(self.violation_with_fix(
file_path,
call.location.line,
call.location.column,
format!(
"Use g_object_notify_by_pspec({}, {}[{}]) instead of g_object_notify({}, \"{}\") for better performance",
obj_str, array_name, enum_value, obj_str, property_name
),
Fix::new(call.location.start_byte, call.location.end_byte, replacement),
));
} else {
let property_constant = self.property_name_to_constant(property_name);
let array_name = &candidates[0].1; violations.push(self.violation(
file_path,
call.location.line,
call.location.column,
format!(
"Use g_object_notify_by_pspec(obj, {}[{}]) instead of g_object_notify(obj, \"{}\") for better performance (ambiguous: multiple classes define this property)",
array_name, property_constant, property_name
),
));
}
} else {
let property_constant = self.property_name_to_constant(property_name);
let suggested_array = if !array_names.is_empty() {
array_names[0]
} else {
"properties"
};
violations.push(self.violation(
file_path,
call.location.line,
call.location.column,
format!(
"Use g_object_notify_by_pspec(obj, {}[{}]) instead of g_object_notify(obj, \"{}\") for better performance",
suggested_array, property_constant, property_name
),
));
}
}
fn build_property_map<'a>(
&self,
file: &'a gobject_ast::FileModel,
) -> HashMap<&'a str, Vec<(&'a str, &'a str, &'a str)>> {
let mut map: HashMap<&str, Vec<(&str, &str, &str)>> = HashMap::new();
for gt in file.iter_all_gobject_types() {
for assignment in >.properties {
if let gobject_ast::ParamSpecAssignment::ArraySubscript {
array_name,
enum_value,
property,
..
} = assignment
{
map.entry(&property.name).or_default().push((
enum_value.as_str(),
array_name.as_str(),
gt.function_prefix.as_str(),
));
}
}
}
map
}
fn disambiguate_by_type<'a>(
&self,
call: &gobject_ast::CallExpression,
source: &[u8],
func: &gobject_ast::top_level::FunctionDefItem,
candidates: &'a [(&'a str, &'a str, &'a str)],
) -> Option<&'a (&'a str, &'a str, &'a str)> {
let obj_expr = call.get_arg(0)?;
let obj_str = obj_expr.to_source_string(source)?;
let obj_identifier = self.extract_identifier(obj_str);
let param_type = func.get_param_by_name(&obj_identifier).and_then(|p| {
if let gobject_ast::model::types::Parameter::Regular { type_info, .. } = p {
Some(&type_info.base_type)
} else {
None
}
})?;
let type_prefix = self.extract_type_prefix(param_type);
candidates
.iter()
.find(|(_, _, class_prefix)| *class_prefix == type_prefix)
}
fn extract_identifier(&self, expr: &str) -> String {
let trimmed = expr.trim();
if let Some(start) = trimmed.rfind('(')
&& let Some(end) = trimmed.rfind(')')
{
return trimmed[start + 1..end].trim().to_string();
}
trimmed.to_string()
}
fn extract_type_prefix(&self, type_name: &str) -> String {
let trimmed = type_name
.trim()
.trim_end_matches('*')
.trim_end_matches("Object")
.trim_end_matches("Class")
.trim();
let mut result = String::new();
for (i, ch) in trimmed.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
result.push('_');
}
result.push(ch.to_ascii_lowercase());
}
result
}
fn property_name_to_constant(&self, property_name: &str) -> String {
let mut result = String::with_capacity(property_name.len() + 5);
result.push_str("PROP_");
for c in property_name.chars() {
if c == '-' {
result.push('_');
} else {
result.push(c.to_ascii_uppercase());
}
}
result
}
}