1use gobject_ast::model::{
2 CallExpression, Expression, FileModel, FunctionDefItem, GObjectType, ParamFlag, Property,
3};
4
5const SINGLE_NAME_ARG: &[(&str, usize)] = &[
6 ("g_object_notify", 1),
7 ("g_object_set_property", 1),
8 ("g_object_get_property", 1),
9 ("g_object_class_find_property", 1),
10 ("g_object_class_override_property", 2),
11 ("gtk_cell_layout_add_attribute", 2),
12 ("gtk_cell_area_cell_get_property", 2),
13];
14const VARARGS_PROP_VALUE: &[(&str, usize)] = &[
15 ("g_object_set", 1),
16 ("g_object_get", 1),
17 ("g_object_new", 1),
18 ("g_object_new_with_properties", 1),
19 ("gtk_show_about_dialog", 1),
20 ("gtk_cell_area_add_with_properties", 2),
21 ("gtk_cell_area_set", 2),
22 ("gtk_cell_area_get", 2),
23 ("gtk_cell_layout_set_attributes", 2),
24 ("gtk_text_buffer_create_tag", 2),
25 ("gtk_tree_view_column_new_with_attributes", 2),
26 ("gtk_tree_view_column_set_attributes", 2),
27 ("gtk_tree_view_insert_column_with_attributes", 4),
28];
29
30use crate::{
31 ast_context::AstContext,
32 config::Config,
33 rules::{Fix, Rule, Violation},
34};
35
36pub struct PropertyCanonicalName;
37
38impl Rule for PropertyCanonicalName {
39 fn name(&self) -> &'static str {
40 "property_canonical_name"
41 }
42
43 fn description(&self) -> &'static str {
44 "Ensure property names are canonical (use dashes, not underscores)"
45 }
46
47 fn category(&self) -> crate::rules::Category {
48 crate::rules::Category::Correctness
49 }
50
51 fn fixable(&self) -> bool {
52 true
53 }
54
55 fn check_gobject_type(
56 &self,
57 _ast_context: &AstContext,
58 _config: &Config,
59 gobject_type: &GObjectType,
60 file: &FileModel,
61 violations: &mut Vec<Violation>,
62 ) {
63 for assignment in &gobject_type.properties {
64 let Some(call) = assignment.param_spec_call() else {
65 continue;
66 };
67 self.check_param_spec_call(file, call, assignment.property(), violations);
68 }
69 }
70
71 fn check_func_impl(
72 &self,
73 _ast_context: &AstContext,
74 _config: &Config,
75 func: &FunctionDefItem,
76 file: &FileModel,
77 violations: &mut Vec<Violation>,
78 ) {
79 for call in func.find_calls_matching(|name| {
80 SINGLE_NAME_ARG.iter().any(|(n, _)| *n == name)
81 || VARARGS_PROP_VALUE.iter().any(|(n, _)| *n == name)
82 }) {
83 let name = call.function_name_str().unwrap();
84
85 if let Some((_, idx)) = SINGLE_NAME_ARG.iter().find(|(n, _)| *n == name) {
86 if let Some(arg) = call.arguments.get(*idx) {
87 self.check_property_name_arg(arg, file, violations);
88 }
89 } else if let Some((_, n_skip)) = VARARGS_PROP_VALUE.iter().find(|(n, _)| *n == name) {
90 for arg in call.arguments.iter().skip(*n_skip).step_by(2) {
91 self.check_property_name_arg(arg, file, violations);
92 }
93 }
94 }
95 }
96}
97
98impl PropertyCanonicalName {
99 fn check_param_spec_call(
100 &self,
101 file: &FileModel,
102 call: &CallExpression,
103 property: &Property,
104 violations: &mut Vec<Violation>,
105 ) {
106 if call.arguments.len() < 2 {
107 return;
108 }
109
110 if !property.name.contains('_') {
111 return;
112 }
113
114 let has_static_name = property.flags.contains(&ParamFlag::StaticName)
115 || property.flags.contains(&ParamFlag::StaticStrings);
116
117 let name_value = &property.name;
118 let canonical_name = name_value.replace('_', "-");
119 let replacement = format!("\"{}\"", canonical_name);
120
121 let Some(expr) = call.get_arg(0) else {
122 return;
123 };
124
125 let string_lit_location = match expr {
126 Expression::StringLiteral(lit) => &lit.location,
127 _ => return,
128 };
129
130 let fix = Fix::new(
131 string_lit_location.start_byte,
132 string_lit_location.end_byte,
133 replacement,
134 );
135
136 let message = if has_static_name {
137 format!(
138 "Property name '{}' is not canonical (contains underscores). \
139 With G_PARAM_STATIC_NAME this will cause: \
140 g_param_spec_internal: assertion '!(flags & G_PARAM_STATIC_NAME) || is_canonical (name)' failed. \
141 Use '{}' instead",
142 name_value, canonical_name
143 )
144 } else {
145 format!(
146 "Property name '{}' should use dashes instead of underscores. \
147 Use '{}' for consistency with GObject conventions",
148 name_value, canonical_name
149 )
150 };
151
152 violations.push(self.violation_with_fix_at(&file.path, string_lit_location, message, fix));
153 }
154
155 fn check_property_name_arg(
156 &self,
157 expr: &Expression,
158 file: &FileModel,
159 violations: &mut Vec<Violation>,
160 ) {
161 let Expression::StringLiteral(string_lit) = expr else {
162 return;
163 };
164 let raw = &string_lit.value;
165
166 let Some(first_close) = raw[1..].find('"') else {
167 return;
168 };
169 let prop_name = &raw[1..1 + first_close];
170
171 if !prop_name.contains('_') {
172 return;
173 }
174
175 let canonical = prop_name.replace('_', "-");
176 let replacement = format!("\"{}\"", canonical);
177
178 let fix = Fix::new(
179 string_lit.location.start_byte,
180 string_lit.location.start_byte + 1 + first_close + 1,
181 replacement,
182 );
183
184 violations.push(self.violation_with_fix_at(
185 &file.path,
186 &string_lit.location,
187 format!(
188 "Property name '{}' should use dashes instead of underscores: '{}'",
189 prop_name, canonical
190 ),
191 fix,
192 ));
193 }
194}