use gobject_ast::{Expression, Statement, top_level::TopLevelItem};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct UseGObjectClassInstallProperties;
impl Rule for UseGObjectClassInstallProperties {
fn name(&self) -> &'static str {
"use_g_object_class_install_properties"
}
fn description(&self) -> &'static str {
"Suggest g_object_class_install_properties for multiple g_object_class_install_property calls"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Complexity
}
fn fixable(&self) -> bool {
true
}
fn check_enum(
&self,
_ast_context: &AstContext,
_config: &Config,
enum_info: &gobject_ast::EnumInfo,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
if !enum_info.is_property_enum() {
return;
}
let Some(gobject_type) = file.find_gobject_type_for_property_enum(enum_info) else {
return;
};
let class_init_name = gobject_type.class_init_function_name();
let Some(func) = file
.iter_function_definitions()
.find(|f| f.name == class_init_name)
else {
return;
};
let install_property_calls = func.find_calls(&["g_object_class_install_property"]);
if install_property_calls.is_empty() {
return;
}
let fixes = self.generate_fixes(
file,
func,
&install_property_calls,
&gobject_type.properties,
enum_info,
&file.source,
);
let first_call = install_property_calls[0];
let message = if fixes.is_empty() {
format!(
"Consider using g_object_class_install_properties() instead of {} g_object_class_install_property() calls",
install_property_calls.len()
)
} else {
format!(
"Use g_object_class_install_properties() instead of {} g_object_class_install_property() calls",
install_property_calls.len()
)
};
violations.push(self.violation_with_fixes(
&file.path,
first_call.location.line,
first_call.location.column,
message,
fixes,
));
}
}
impl UseGObjectClassInstallProperties {
fn generate_fixes(
&self,
file: &gobject_ast::FileModel,
class_init: &gobject_ast::top_level::FunctionDefItem,
install_calls: &[&gobject_ast::CallExpression],
assignments: &[gobject_ast::ParamSpecAssignment],
property_enum: &gobject_ast::EnumInfo,
source: &[u8],
) -> Vec<Fix> {
let mut fixes = Vec::new();
let param_spec_assignments: Vec<_> = assignments
.iter()
.filter_map(|a| {
if let gobject_ast::ParamSpecAssignment::Variable {
variable_name,
statement_location,
call,
..
} = a
{
Some((variable_name, statement_location, call))
} else {
None
}
})
.collect();
let n_props_value = property_enum.values.iter().find(|v| v.is_prop_last());
let n_props_name = if let Some(n_props) = n_props_value {
n_props.name.clone()
} else {
let n_props_name = self.determine_n_props_name(property_enum);
let last_value = property_enum.values.last().unwrap();
let value_indentation = last_value.location.extract_indentation(source);
let (insertion_pos, needs_comma) = if last_value.location.end_byte < source.len()
&& source[last_value.location.end_byte] == b','
{
(last_value.location.end_byte + 1, false)
} else {
(last_value.location.end_byte, true)
};
let n_props_decl = if needs_comma {
format!(",\n{}{}", value_indentation, n_props_name)
} else {
format!("\n{}{}", value_indentation, n_props_name)
};
fixes.push(Fix::new(insertion_pos, insertion_pos, n_props_decl));
n_props_name
};
let array_name = self.determine_array_name(file, source);
let insertion_pos = if property_enum.location.end_byte < source.len()
&& source[property_enum.location.end_byte] == b';'
{
property_enum.location.end_byte + 1
} else {
property_enum.location.end_byte
};
let array_decl = format!(
"\n\nstatic GParamSpec *{}[{}] = {{ NULL, }};",
array_name, n_props_name
);
fixes.push(Fix::new(insertion_pos, insertion_pos, array_decl));
let object_class_var = class_init
.iter_local_declarations()
.find(|decl| decl.type_info.base_type == "GObjectClass")
.map_or("object_class", |decl| decl.name.as_str());
let indentation = if let Some(first_call) = install_calls.first() {
if let Some(stmt) = self.find_statement_containing_call(
&class_init.body_statements,
first_call.location.start_byte,
) {
stmt.location().extract_indentation(source)
} else {
" ".to_string()
}
} else {
" ".to_string()
};
let mut param_spec_vars = std::collections::HashSet::new();
for call in install_calls {
let Some(prop_id_arg) = call.get_arg(1) else {
continue;
};
let Some(prop_id) = prop_id_arg.to_source_string(source) else {
continue;
};
let Some(param_spec_arg) = call.get_arg(2) else {
continue;
};
let (param_spec, delete_install_call) = if let Expression::Call(param_spec_call) =
param_spec_arg
{
let func_name = param_spec_call.function_name(source);
let new_line_prefix = format!("{}[{}] = {} (", array_name, prop_id, func_name);
let target_column = indentation.len() + new_line_prefix.len();
let Some(param_spec_text) = param_spec_arg.to_source_string(source) else {
continue;
};
(
self.reindent_multiline(param_spec_text, target_column),
false,
)
} else {
let Some(var_name) = param_spec_arg.to_source_string(source) else {
continue;
};
let assignment = param_spec_assignments
.iter()
.filter(|(name, stmt_loc, _)| {
name.as_str() == var_name && stmt_loc.start_byte < call.location.start_byte
})
.max_by_key(|(_, stmt_loc, _)| stmt_loc.start_byte);
if let Some((_, statement_location, g_param_spec_call)) = assignment {
param_spec_vars.insert(var_name);
let func_name = g_param_spec_call.function_name(source);
let new_line_prefix = format!("{}[{}] = {} (", array_name, prop_id, func_name);
let assignment_indent = statement_location.extract_indentation(source);
let target_column = assignment_indent.len() + new_line_prefix.len();
let Some(param_spec_text) =
Expression::Call((*g_param_spec_call).clone()).to_source_string(source)
else {
continue;
};
let replacement = format!(
"{}[{}] = {};",
array_name,
prop_id,
self.reindent_multiline(param_spec_text, target_column)
);
fixes.push(Fix::new(
statement_location.start_byte,
statement_location.find_semicolon_end(source),
replacement,
));
(String::new(), true) } else {
let Some(param_spec_text) = param_spec_arg.to_source_string(source) else {
continue;
};
(param_spec_text.to_owned(), false)
}
};
let Some(stmt) = self.find_statement_containing_call(
&class_init.body_statements,
call.location.start_byte,
) else {
continue;
};
if delete_install_call {
fixes.push(Fix::delete_line(stmt.location(), source));
} else {
let replacement = format!("{}[{}] = {};", array_name, prop_id, param_spec);
fixes.push(Fix::new(
stmt.location().start_byte,
stmt.location().find_semicolon_end(source),
replacement,
));
}
}
for var_name in param_spec_vars {
if let Some(decl) = class_init
.body_statements
.iter()
.flat_map(gobject_ast::Statement::iter_declarations)
.find(|decl| decl.name == var_name && decl.type_info.base_type == "GParamSpec")
{
fixes.push(Fix::delete_line(&decl.location, source));
}
}
if let Some(last_call) = install_calls.last() {
let Some(last_stmt) = self.find_statement_containing_call(
&class_init.body_statements,
last_call.location.start_byte,
) else {
return fixes;
};
let install_properties_call = format!(
"\n\n{}g_object_class_install_properties ({}, {}, {});",
indentation, object_class_var, n_props_name, array_name
);
let last_stmt_end = last_stmt.location().find_semicolon_end(source);
fixes.push(Fix::new(
last_stmt_end,
last_stmt_end,
install_properties_call,
));
}
fixes
}
fn determine_n_props_name(&self, property_enum: &gobject_ast::EnumInfo) -> String {
if let Some(first_value) = property_enum.values.first() {
let name = &first_value.name;
if let Some(prefix_end) = name.rfind("PROP_") {
let prefix = &name[..prefix_end];
if prefix.is_empty() {
return "N_PROPS".to_string();
} else {
return format!("{}N_PROPS", prefix);
}
}
}
"N_PROPS".to_string()
}
fn determine_array_name(&self, file: &gobject_ast::FileModel, _source: &[u8]) -> String {
for item in &file.top_level_items {
if let TopLevelItem::Declaration(decl) = item
&& decl.name == "props"
&& decl.type_info.full_text.contains("GParamSpec")
{
return "obj_props".to_string();
}
}
"props".to_string()
}
fn find_statement_containing_call<'a>(
&self,
statements: &'a [Statement],
call_start_byte: usize,
) -> Option<&'a Statement> {
for stmt in statements {
let loc = stmt.location();
if call_start_byte >= loc.start_byte && call_start_byte < loc.end_byte {
return Some(stmt);
}
}
None
}
fn reindent_multiline(&self, text: &str, target_column: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= 1 {
return text.to_string();
}
let continuation_indent = " ".repeat(target_column);
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
if i == 0 {
result.push_str(line);
} else {
result.push('\n');
result.push_str(&continuation_indent);
result.push_str(line.trim_start());
}
}
result
}
}