Skip to main content

makefile_lossless/ast/
variable.rs

1use super::makefile::MakefileItem;
2use crate::lossless::{remove_with_preceding_comments, VariableDefinition};
3use crate::SyntaxKind::*;
4use rowan::ast::AstNode;
5use rowan::{GreenNodeBuilder, SyntaxNode};
6
7/// Recursively rebuild a syntax node into a GreenNodeBuilder.
8fn 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    /// Get the name of the variable definition
25    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    /// Check if this variable definition is exported
38    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    /// Get the assignment operator/flavor used in this variable definition
45    ///
46    /// Returns the operator as a string: "=", ":=", "::=", ":::=", "+=", "?=", or "!="
47    ///
48    /// # Example
49    /// ```
50    /// use makefile_lossless::Makefile;
51    /// let makefile: Makefile = "VAR := value\n".parse().unwrap();
52    /// let var = makefile.variable_definitions().next().unwrap();
53    /// assert_eq!(var.assignment_operator(), Some(":=".to_string()));
54    /// ```
55    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    /// Get the raw value of the variable definition
68    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    /// Get the parent item of this variable definition, if any
76    ///
77    /// Returns `Some(MakefileItem)` if this variable has a parent that is a MakefileItem
78    /// (e.g., a Conditional), or `None` if the parent is the root Makefile node.
79    ///
80    /// # Example
81    /// ```
82    /// use makefile_lossless::Makefile;
83    ///
84    /// let makefile: Makefile = r#"ifdef DEBUG
85    /// VAR = value
86    /// endif
87    /// "#.parse().unwrap();
88    /// let cond = makefile.conditionals().next().unwrap();
89    /// let var = cond.if_items().next().unwrap();
90    /// // Variable's parent is the conditional
91    /// assert!(matches!(var, makefile_lossless::MakefileItem::Variable(_)));
92    /// ```
93    pub fn parent(&self) -> Option<MakefileItem> {
94        self.syntax().parent().and_then(MakefileItem::cast)
95    }
96
97    /// Remove this variable definition from its parent makefile
98    ///
99    /// This will also remove any preceding comments and up to 1 empty line before the variable.
100    ///
101    /// # Example
102    /// ```
103    /// use makefile_lossless::Makefile;
104    /// let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
105    /// let mut var = makefile.variable_definitions().next().unwrap();
106    /// var.remove();
107    /// assert_eq!(makefile.variable_definitions().count(), 0);
108    /// ```
109    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    /// Change the assignment operator of this variable definition while preserving everything else
116    /// (export prefix, variable name, value, whitespace, etc.)
117    ///
118    /// # Arguments
119    /// * `op` - The new operator: "=", ":=", "::=", ":::=", "+=", "?=", or "!="
120    ///
121    /// # Example
122    /// ```
123    /// use makefile_lossless::Makefile;
124    /// let mut makefile: Makefile = "VAR := value\n".parse().unwrap();
125    /// let mut var = makefile.variable_definitions().next().unwrap();
126    /// var.set_assignment_operator("?=");
127    /// assert_eq!(var.assignment_operator(), Some("?=".to_string()));
128    /// assert!(makefile.code().contains("VAR ?= value"));
129    /// ```
130    pub fn set_assignment_operator(&mut self, op: &str) {
131        // Build a new VARIABLE node, copying all children but replacing the OPERATOR token
132        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        // Replace the old VARIABLE node with the new one
153        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            // Update self to point to the new node
158            *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    /// Update the value of this variable definition while preserving the rest
170    /// (export prefix, operator, whitespace, etc.)
171    ///
172    /// # Example
173    /// ```
174    /// use makefile_lossless::Makefile;
175    /// let mut makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
176    /// let mut var = makefile.variable_definitions().next().unwrap();
177    /// var.set_value("new_value");
178    /// assert_eq!(var.raw_value(), Some("new_value".to_string()));
179    /// assert!(makefile.code().contains("export VAR := new_value"));
180    /// ```
181    pub fn set_value(&mut self, new_value: &str) {
182        // Find the EXPR node containing the value
183        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            // Build a new EXPR node with the new value
191            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            // Replace the old EXPR with the new one
199            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        // Parent is ROOT node which doesn't cast to MakefileItem
217        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        // Change operator
317        var.set_assignment_operator("?=");
318        assert_eq!(var.assignment_operator(), Some("?=".to_string()));
319
320        // Change value
321        var.set_value("new_value");
322        assert_eq!(var.raw_value(), Some("new_value".to_string()));
323
324        // Verify everything
325        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}