Skip to main content

gobject_linter/rules/
property_canonical_name.rs

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}