Skip to main content

gobject_linter/rules/
g_param_spec_static_strings.rs

1use std::sync::LazyLock;
2
3use gobject_ast::model::{CallExpression, FileModel, GObjectType, ParamFlag, Property};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{ConfigOption, Fix, Rule, Violation},
9};
10
11pub struct GParamSpecStaticStrings;
12
13impl Rule for GParamSpecStaticStrings {
14    fn name(&self) -> &'static str {
15        "g_param_spec_static_strings"
16    }
17
18    fn description(&self) -> &'static str {
19        "Ensure *_param_spec_* calls use G_PARAM_STATIC_STRINGS flag for string literals"
20    }
21
22    fn category(&self) -> crate::rules::Category {
23        crate::rules::Category::Perf
24    }
25
26    fn fixable(&self) -> bool {
27        true
28    }
29
30    fn config_options(&self) -> &'static [ConfigOption] {
31        static OPTIONS: LazyLock<Vec<ConfigOption>> = LazyLock::new(|| {
32            vec![ConfigOption {
33                name: "static_flags",
34                option_type: "array<string>",
35                default_value: "[]",
36                example_value: "[\"ST_PARAM_READWRITE\", \"ST_PARAM_READABLE\"]",
37                description: "List of custom flag constants that already include G_PARAM_STATIC_STRINGS",
38            }]
39        });
40
41        &OPTIONS
42    }
43
44    fn check_gobject_type(
45        &self,
46        _ast_context: &AstContext,
47        config: &Config,
48        gobject_type: &GObjectType,
49        file: &FileModel,
50        violations: &mut Vec<Violation>,
51    ) {
52        let static_flags = config.get_string_list(self.name(), "static_flags");
53
54        for assignment in &gobject_type.properties {
55            let Some(call) = assignment.param_spec_call() else {
56                continue;
57            };
58            self.check_call(file, call, assignment.property(), &static_flags, violations);
59        }
60    }
61}
62
63impl GParamSpecStaticStrings {
64    fn check_call(
65        &self,
66        file: &FileModel,
67        call: &CallExpression,
68        property: &Property,
69        custom_static_flags: &[String],
70        violations: &mut Vec<Violation>,
71    ) {
72        if call.arguments.len() < 4 {
73            return;
74        }
75
76        let nick_is_literal = property.nick.is_some();
77        let blurb_is_literal = property.blurb.is_some();
78
79        let has_static_strings = property.flags.contains(&ParamFlag::StaticStrings);
80        let has_static_name = property.flags.contains(&ParamFlag::StaticName);
81        let has_static_nick = property.flags.contains(&ParamFlag::StaticNick);
82        let has_static_blurb = property.flags.contains(&ParamFlag::StaticBlurb);
83
84        let has_custom_static_flag = property.flags.iter().any(|flag| {
85            if let ParamFlag::Unknown(name) = flag {
86                custom_static_flags.contains(name)
87            } else {
88                false
89            }
90        });
91
92        let is_satisfied = if has_static_strings || has_custom_static_flag {
93            true
94        } else if nick_is_literal && blurb_is_literal {
95            has_static_name && has_static_nick && has_static_blurb
96        } else if nick_is_literal {
97            has_static_name && has_static_nick
98        } else if blurb_is_literal {
99            has_static_name && has_static_blurb
100        } else {
101            has_static_name
102        };
103
104        if is_satisfied {
105            return;
106        }
107
108        let needed = self.needed_flags(nick_is_literal, blurb_is_literal);
109        let new_flags = self.build_fixed_flags(&property.flags, &needed);
110        let needed_desc = needed
111            .iter()
112            .map(ParamFlag::as_str)
113            .collect::<Vec<_>>()
114            .join(" | ");
115
116        let flags_expr = call.arguments.last().unwrap();
117        let fix = Fix::new(
118            flags_expr.location().start_byte,
119            flags_expr.location().end_byte,
120            new_flags,
121        );
122
123        violations.push(self.violation_with_fix_at(
124            &file.path,
125            &call.location,
126            format!(
127                "Add {} to {} flags (saves memory for static strings)",
128                needed_desc,
129                call.function_name()
130            ),
131            fix,
132        ));
133    }
134
135    fn needed_flags(&self, nick_is_literal: bool, blurb_is_literal: bool) -> Vec<ParamFlag> {
136        match (nick_is_literal, blurb_is_literal) {
137            (true, true) => vec![ParamFlag::StaticStrings],
138            (true, false) => vec![ParamFlag::StaticName, ParamFlag::StaticNick],
139            (false, true) => vec![ParamFlag::StaticName, ParamFlag::StaticBlurb],
140            (false, false) => vec![ParamFlag::StaticName],
141        }
142    }
143
144    fn build_fixed_flags(&self, current_flags: &[ParamFlag], needed_flags: &[ParamFlag]) -> String {
145        let mut new_flags: Vec<ParamFlag> = current_flags
146            .iter()
147            .filter(|f| {
148                !matches!(
149                    f,
150                    ParamFlag::StaticName
151                        | ParamFlag::StaticNick
152                        | ParamFlag::StaticBlurb
153                        | ParamFlag::StaticStrings
154                )
155            })
156            .cloned()
157            .collect();
158
159        new_flags.extend_from_slice(needed_flags);
160
161        if new_flags.is_empty() {
162            "0".to_string()
163        } else {
164            new_flags
165                .iter()
166                .map(ParamFlag::as_str)
167                .collect::<Vec<_>>()
168                .join(" | ")
169        }
170    }
171}