makefile_lossless/ast/
variable.rs1use super::makefile::MakefileItem;
2use crate::lossless::{remove_with_preceding_comments, VariableDefinition};
3use crate::SyntaxKind::*;
4use rowan::ast::AstNode;
5use rowan::{GreenNodeBuilder, SyntaxNode};
6
7fn rebuild_node(builder: &mut GreenNodeBuilder, node: &crate::lossless::SyntaxNode) {
9 builder.start_node(node.kind().into());
10 for child in node.children_with_tokens() {
11 match child {
12 rowan::NodeOrToken::Token(token) => {
13 builder.token(token.kind().into(), token.text());
14 }
15 rowan::NodeOrToken::Node(child_node) => {
16 rebuild_node(builder, &child_node);
17 }
18 }
19 }
20 builder.finish_node();
21}
22
23impl VariableDefinition {
24 pub fn name(&self) -> Option<String> {
26 self.syntax().children_with_tokens().find_map(|it| {
27 it.as_token().and_then(|it| {
28 if it.kind() == IDENTIFIER && it.text() != "export" {
29 Some(it.text().to_string())
30 } else {
31 None
32 }
33 })
34 })
35 }
36
37 pub fn is_export(&self) -> bool {
39 self.syntax()
40 .children_with_tokens()
41 .any(|it| it.as_token().is_some_and(|token| token.text() == "export"))
42 }
43
44 pub fn assignment_operator(&self) -> Option<String> {
56 self.syntax().children_with_tokens().find_map(|it| {
57 it.as_token().and_then(|token| {
58 if token.kind() == OPERATOR {
59 Some(token.text().to_string())
60 } else {
61 None
62 }
63 })
64 })
65 }
66
67 pub fn raw_value(&self) -> Option<String> {
69 self.syntax()
70 .children()
71 .find(|it| it.kind() == EXPR)
72 .map(|it| it.text().into())
73 }
74
75 pub fn parent(&self) -> Option<MakefileItem> {
94 self.syntax().parent().and_then(MakefileItem::cast)
95 }
96
97 pub fn remove(&mut self) {
110 if let Some(parent) = self.syntax().parent() {
111 remove_with_preceding_comments(self.syntax(), &parent);
112 }
113 }
114
115 pub fn set_assignment_operator(&mut self, op: &str) {
131 let mut builder = GreenNodeBuilder::new();
133 builder.start_node(VARIABLE.into());
134
135 for child in self.syntax().children_with_tokens() {
136 match child {
137 rowan::NodeOrToken::Token(token) if token.kind() == OPERATOR => {
138 builder.token(OPERATOR.into(), op);
139 }
140 rowan::NodeOrToken::Token(token) => {
141 builder.token(token.kind().into(), token.text());
142 }
143 rowan::NodeOrToken::Node(node) => {
144 rebuild_node(&mut builder, &node);
145 }
146 }
147 }
148
149 builder.finish_node();
150 let new_variable = SyntaxNode::new_root_mut(builder.finish());
151
152 let index = self.syntax().index();
154 if let Some(parent) = self.syntax().parent() {
155 parent.splice_children(index..index + 1, vec![new_variable.clone().into()]);
156
157 *self = VariableDefinition::cast(
159 parent
160 .children_with_tokens()
161 .nth(index)
162 .and_then(|it| it.into_node())
163 .unwrap(),
164 )
165 .unwrap();
166 }
167 }
168
169 pub fn set_value(&mut self, new_value: &str) {
182 let expr_index = self
184 .syntax()
185 .children()
186 .find(|it| it.kind() == EXPR)
187 .map(|it| it.index());
188
189 if let Some(expr_idx) = expr_index {
190 let mut builder = GreenNodeBuilder::new();
192 builder.start_node(EXPR.into());
193 builder.token(IDENTIFIER.into(), new_value);
194 builder.finish_node();
195
196 let new_expr = SyntaxNode::new_root_mut(builder.finish());
197
198 self.syntax()
200 .splice_children(expr_idx..expr_idx + 1, vec![new_expr.into()]);
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207
208 use crate::lossless::Makefile;
209
210 #[test]
211 fn test_variable_parent() {
212 let makefile: Makefile = "VAR = value\n".parse().unwrap();
213
214 let var = makefile.variable_definitions().next().unwrap();
215 let parent = var.parent();
216 assert!(parent.is_none());
218 }
219
220 #[test]
221 fn test_assignment_operator_simple() {
222 let makefile: Makefile = "VAR = value\n".parse().unwrap();
223 let var = makefile.variable_definitions().next().unwrap();
224 assert_eq!(var.assignment_operator(), Some("=".to_string()));
225 }
226
227 #[test]
228 fn test_assignment_operator_recursive() {
229 let makefile: Makefile = "VAR := value\n".parse().unwrap();
230 let var = makefile.variable_definitions().next().unwrap();
231 assert_eq!(var.assignment_operator(), Some(":=".to_string()));
232 }
233
234 #[test]
235 fn test_assignment_operator_conditional() {
236 let makefile: Makefile = "VAR ?= value\n".parse().unwrap();
237 let var = makefile.variable_definitions().next().unwrap();
238 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
239 }
240
241 #[test]
242 fn test_assignment_operator_append() {
243 let makefile: Makefile = "VAR += value\n".parse().unwrap();
244 let var = makefile.variable_definitions().next().unwrap();
245 assert_eq!(var.assignment_operator(), Some("+=".to_string()));
246 }
247
248 #[test]
249 fn test_assignment_operator_export() {
250 let makefile: Makefile = "export VAR := value\n".parse().unwrap();
251 let var = makefile.variable_definitions().next().unwrap();
252 assert_eq!(var.assignment_operator(), Some(":=".to_string()));
253 }
254
255 #[test]
256 fn test_set_assignment_operator_simple_to_conditional() {
257 let makefile: Makefile = "VAR = value\n".parse().unwrap();
258 let mut var = makefile.variable_definitions().next().unwrap();
259 var.set_assignment_operator("?=");
260 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
261 assert_eq!(makefile.code(), "VAR ?= value\n");
262 }
263
264 #[test]
265 fn test_set_assignment_operator_recursive_to_conditional() {
266 let makefile: Makefile = "VAR := value\n".parse().unwrap();
267 let mut var = makefile.variable_definitions().next().unwrap();
268 var.set_assignment_operator("?=");
269 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
270 assert_eq!(makefile.code(), "VAR ?= value\n");
271 }
272
273 #[test]
274 fn test_set_assignment_operator_preserves_export() {
275 let makefile: Makefile = "export VAR := value\n".parse().unwrap();
276 let mut var = makefile.variable_definitions().next().unwrap();
277 var.set_assignment_operator("?=");
278 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
279 assert!(var.is_export());
280 assert_eq!(makefile.code(), "export VAR ?= value\n");
281 }
282
283 #[test]
284 fn test_set_assignment_operator_preserves_whitespace() {
285 let makefile: Makefile = "VAR := value\n".parse().unwrap();
286 let mut var = makefile.variable_definitions().next().unwrap();
287 var.set_assignment_operator("?=");
288 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
289 assert_eq!(makefile.code(), "VAR ?= value\n");
290 }
291
292 #[test]
293 fn test_set_assignment_operator_preserves_value() {
294 let makefile: Makefile = "VAR := old_value\n".parse().unwrap();
295 let mut var = makefile.variable_definitions().next().unwrap();
296 var.set_assignment_operator("=");
297 assert_eq!(var.assignment_operator(), Some("=".to_string()));
298 assert_eq!(var.raw_value(), Some("old_value".to_string()));
299 assert_eq!(makefile.code(), "VAR = old_value\n");
300 }
301
302 #[test]
303 fn test_set_assignment_operator_to_triple_colon() {
304 let makefile: Makefile = "VAR := value\n".parse().unwrap();
305 let mut var = makefile.variable_definitions().next().unwrap();
306 var.set_assignment_operator("::=");
307 assert_eq!(var.assignment_operator(), Some("::=".to_string()));
308 assert_eq!(makefile.code(), "VAR ::= value\n");
309 }
310
311 #[test]
312 fn test_combined_operations() {
313 let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
314 let mut var = makefile.variable_definitions().next().unwrap();
315
316 var.set_assignment_operator("?=");
318 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
319
320 var.set_value("new_value");
322 assert_eq!(var.raw_value(), Some("new_value".to_string()));
323
324 assert!(var.is_export());
326 assert_eq!(var.name(), Some("VAR".to_string()));
327 assert_eq!(makefile.code(), "export VAR ?= new_value\n");
328 }
329
330 #[test]
331 fn test_set_assignment_operator_preserves_shell_call() {
332 let makefile: Makefile = "DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)\n"
333 .parse()
334 .unwrap();
335 let mut var = makefile.variable_definitions().next().unwrap();
336 var.set_assignment_operator("?=");
337 assert_eq!(var.assignment_operator(), Some("?=".to_string()));
338 assert_eq!(
339 makefile.code(),
340 "DEB_HOST_ARCH ?= $(shell dpkg-architecture -qDEB_HOST_ARCH)\n"
341 );
342 }
343}