Skip to main content

gobject_linter/rules/
signal_canonical_name.rs

1use gobject_ast::model::{Expression, FileModel, FunctionDefItem};
2
3use crate::{
4    ast_context::AstContext,
5    config::Config,
6    rules::{Fix, Rule, Violation},
7};
8
9pub struct SignalCanonicalName;
10
11impl Rule for SignalCanonicalName {
12    fn name(&self) -> &'static str {
13        "signal_canonical_name"
14    }
15
16    fn description(&self) -> &'static str {
17        "Signal names should use hyphens (-) instead of underscores (_)"
18    }
19
20    fn category(&self) -> crate::rules::Category {
21        crate::rules::Category::Style
22    }
23
24    fn fixable(&self) -> bool {
25        true
26    }
27
28    fn check_func_impl(
29        &self,
30        _ast_context: &AstContext,
31        _config: &Config,
32        func: &FunctionDefItem,
33        file: &FileModel,
34        violations: &mut Vec<Violation>,
35    ) {
36        const NAME_ARG_FIRST: &[&str] = &[
37            "g_signal_new",
38            "g_signal_newv",
39            "g_signal_new_valist",
40            "g_signal_new_class_handler",
41            "g_signal_lookup",
42        ];
43        const NAME_ARG_SECOND: &[&str] = &[
44            "g_signal_connect",
45            "g_signal_connect_after",
46            "g_signal_connect_swapped",
47            "g_signal_connect_data",
48            "g_signal_connect_object",
49            "g_signal_emit_by_name",
50            "g_signal_stop_emission_by_name",
51            "g_signal_group_connect",
52            "g_signal_group_connect_after",
53            "g_signal_group_connect_swapped",
54            "g_signal_group_connect_object",
55        ];
56
57        for call in func.find_calls_matching(|name| {
58            NAME_ARG_FIRST.contains(&name) || NAME_ARG_SECOND.contains(&name)
59        }) {
60            let name = call.function_name_str().unwrap();
61            let arg_index = if NAME_ARG_FIRST.contains(&name) { 0 } else { 1 };
62            if let Some(arg_expr) = call.arguments.get(arg_index) {
63                self.check_signal_name_arg(arg_expr, file, violations);
64            }
65        }
66    }
67}
68
69impl SignalCanonicalName {
70    fn check_signal_name_arg(
71        &self,
72        expr: &Expression,
73        file: &FileModel,
74        violations: &mut Vec<Violation>,
75    ) {
76        if let Expression::StringLiteral(string_lit) = expr {
77            let raw = &string_lit.value;
78
79            // Find the content of the first quoted portion.
80            let Some(first_close) = raw[1..].find('"') else {
81                return;
82            };
83            let first_str_content = &raw[1..1 + first_close];
84
85            let signal_name = first_str_content
86                .split("::")
87                .next()
88                .unwrap_or(first_str_content);
89
90            if signal_name.contains('_') {
91                let fixed_signal = signal_name.replace('_', "-");
92
93                let fixed_first_str = format!(
94                    "\"{}{}\"",
95                    fixed_signal,
96                    &first_str_content[signal_name.len()..],
97                );
98
99                let fix_start = string_lit.location.start_byte;
100                let fix_end = fix_start + 1 + first_close + 1;
101
102                let fix = Fix::new(fix_start, fix_end, fixed_first_str);
103
104                violations.push(self.violation_with_fix_at(
105                    &file.path,
106                    &string_lit.location,
107                    format!(
108                        "Signal name '{}' should use hyphens instead of underscores: '{}'",
109                        signal_name, fixed_signal
110                    ),
111                    fix,
112                ));
113            }
114        }
115    }
116}