gobject_linter/rules/
g_param_spec_static_strings.rs1use 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}