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
7impl VariableDefinition {
8    /// Get the name of the variable definition
9    pub fn name(&self) -> Option<String> {
10        self.syntax().children_with_tokens().find_map(|it| {
11            it.as_token().and_then(|it| {
12                if it.kind() == IDENTIFIER && it.text() != "export" {
13                    Some(it.text().to_string())
14                } else {
15                    None
16                }
17            })
18        })
19    }
20
21    /// Check if this variable definition is exported
22    pub fn is_export(&self) -> bool {
23        self.syntax()
24            .children_with_tokens()
25            .any(|it| it.as_token().is_some_and(|token| token.text() == "export"))
26    }
27
28    /// Get the assignment operator/flavor used in this variable definition
29    ///
30    /// Returns the operator as a string: "=", ":=", "::=", ":::=", "+=", "?=", or "!="
31    ///
32    /// # Example
33    /// ```
34    /// use makefile_lossless::Makefile;
35    /// let makefile: Makefile = "VAR := value\n".parse().unwrap();
36    /// let var = makefile.variable_definitions().next().unwrap();
37    /// assert_eq!(var.assignment_operator(), Some(":=".to_string()));
38    /// ```
39    pub fn assignment_operator(&self) -> Option<String> {
40        self.syntax().children_with_tokens().find_map(|it| {
41            it.as_token().and_then(|token| {
42                if token.kind() == OPERATOR {
43                    Some(token.text().to_string())
44                } else {
45                    None
46                }
47            })
48        })
49    }
50
51    /// Get the raw value of the variable definition
52    pub fn raw_value(&self) -> Option<String> {
53        self.syntax()
54            .children()
55            .find(|it| it.kind() == EXPR)
56            .map(|it| it.text().into())
57    }
58
59    /// Get the parent item of this variable definition, if any
60    ///
61    /// Returns `Some(MakefileItem)` if this variable has a parent that is a MakefileItem
62    /// (e.g., a Conditional), or `None` if the parent is the root Makefile node.
63    ///
64    /// # Example
65    /// ```
66    /// use makefile_lossless::Makefile;
67    ///
68    /// let makefile: Makefile = r#"ifdef DEBUG
69    /// VAR = value
70    /// endif
71    /// "#.parse().unwrap();
72    /// let cond = makefile.conditionals().next().unwrap();
73    /// let var = cond.if_items().next().unwrap();
74    /// // Variable's parent is the conditional
75    /// assert!(matches!(var, makefile_lossless::MakefileItem::Variable(_)));
76    /// ```
77    pub fn parent(&self) -> Option<MakefileItem> {
78        self.syntax().parent().and_then(MakefileItem::cast)
79    }
80
81    /// Remove this variable definition from its parent makefile
82    ///
83    /// This will also remove any preceding comments and up to 1 empty line before the variable.
84    ///
85    /// # Example
86    /// ```
87    /// use makefile_lossless::Makefile;
88    /// let mut makefile: Makefile = "VAR = value\n".parse().unwrap();
89    /// let mut var = makefile.variable_definitions().next().unwrap();
90    /// var.remove();
91    /// assert_eq!(makefile.variable_definitions().count(), 0);
92    /// ```
93    pub fn remove(&mut self) {
94        if let Some(parent) = self.syntax().parent() {
95            remove_with_preceding_comments(self.syntax(), &parent);
96        }
97    }
98
99    /// Change the assignment operator of this variable definition while preserving everything else
100    /// (export prefix, variable name, value, whitespace, etc.)
101    ///
102    /// # Arguments
103    /// * `op` - The new operator: "=", ":=", "::=", ":::=", "+=", "?=", or "!="
104    ///
105    /// # Example
106    /// ```
107    /// use makefile_lossless::Makefile;
108    /// let mut makefile: Makefile = "VAR := value\n".parse().unwrap();
109    /// let mut var = makefile.variable_definitions().next().unwrap();
110    /// var.set_assignment_operator("?=");
111    /// assert_eq!(var.assignment_operator(), Some("?=".to_string()));
112    /// assert!(makefile.code().contains("VAR ?= value"));
113    /// ```
114    pub fn set_assignment_operator(&mut self, op: &str) {
115        // Build a new VARIABLE node, copying all children but replacing the OPERATOR token
116        let mut builder = GreenNodeBuilder::new();
117        builder.start_node(VARIABLE.into());
118
119        for child in self.syntax().children_with_tokens() {
120            match child {
121                rowan::NodeOrToken::Token(token) if token.kind() == OPERATOR => {
122                    builder.token(OPERATOR.into(), op);
123                }
124                rowan::NodeOrToken::Token(token) => {
125                    builder.token(token.kind().into(), token.text());
126                }
127                rowan::NodeOrToken::Node(node) => {
128                    // For nodes (like EXPR), rebuild them by iterating their structure
129                    builder.start_node(node.kind().into());
130                    for node_child in node.children_with_tokens() {
131                        if let rowan::NodeOrToken::Token(token) = node_child {
132                            builder.token(token.kind().into(), token.text());
133                        }
134                    }
135                    builder.finish_node();
136                }
137            }
138        }
139
140        builder.finish_node();
141        let new_variable = SyntaxNode::new_root_mut(builder.finish());
142
143        // Replace the old VARIABLE node with the new one
144        let index = self.syntax().index();
145        if let Some(parent) = self.syntax().parent() {
146            parent.splice_children(index..index + 1, vec![new_variable.clone().into()]);
147
148            // Update self to point to the new node
149            *self = VariableDefinition::cast(
150                parent
151                    .children_with_tokens()
152                    .nth(index)
153                    .and_then(|it| it.into_node())
154                    .unwrap(),
155            )
156            .unwrap();
157        }
158    }
159
160    /// Update the value of this variable definition while preserving the rest
161    /// (export prefix, operator, whitespace, etc.)
162    ///
163    /// # Example
164    /// ```
165    /// use makefile_lossless::Makefile;
166    /// let mut makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
167    /// let mut var = makefile.variable_definitions().next().unwrap();
168    /// var.set_value("new_value");
169    /// assert_eq!(var.raw_value(), Some("new_value".to_string()));
170    /// assert!(makefile.code().contains("export VAR := new_value"));
171    /// ```
172    pub fn set_value(&mut self, new_value: &str) {
173        // Find the EXPR node containing the value
174        let expr_index = self
175            .syntax()
176            .children()
177            .find(|it| it.kind() == EXPR)
178            .map(|it| it.index());
179
180        if let Some(expr_idx) = expr_index {
181            // Build a new EXPR node with the new value
182            let mut builder = GreenNodeBuilder::new();
183            builder.start_node(EXPR.into());
184            builder.token(IDENTIFIER.into(), new_value);
185            builder.finish_node();
186
187            let new_expr = SyntaxNode::new_root_mut(builder.finish());
188
189            // Replace the old EXPR with the new one
190            self.syntax()
191                .splice_children(expr_idx..expr_idx + 1, vec![new_expr.into()]);
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198
199    use crate::lossless::Makefile;
200
201    #[test]
202    fn test_variable_parent() {
203        let makefile: Makefile = "VAR = value\n".parse().unwrap();
204
205        let var = makefile.variable_definitions().next().unwrap();
206        let parent = var.parent();
207        // Parent is ROOT node which doesn't cast to MakefileItem
208        assert!(parent.is_none());
209    }
210
211    #[test]
212    fn test_assignment_operator_simple() {
213        let makefile: Makefile = "VAR = value\n".parse().unwrap();
214        let var = makefile.variable_definitions().next().unwrap();
215        assert_eq!(var.assignment_operator(), Some("=".to_string()));
216    }
217
218    #[test]
219    fn test_assignment_operator_recursive() {
220        let makefile: Makefile = "VAR := value\n".parse().unwrap();
221        let var = makefile.variable_definitions().next().unwrap();
222        assert_eq!(var.assignment_operator(), Some(":=".to_string()));
223    }
224
225    #[test]
226    fn test_assignment_operator_conditional() {
227        let makefile: Makefile = "VAR ?= value\n".parse().unwrap();
228        let var = makefile.variable_definitions().next().unwrap();
229        assert_eq!(var.assignment_operator(), Some("?=".to_string()));
230    }
231
232    #[test]
233    fn test_assignment_operator_append() {
234        let makefile: Makefile = "VAR += value\n".parse().unwrap();
235        let var = makefile.variable_definitions().next().unwrap();
236        assert_eq!(var.assignment_operator(), Some("+=".to_string()));
237    }
238
239    #[test]
240    fn test_assignment_operator_export() {
241        let makefile: Makefile = "export VAR := value\n".parse().unwrap();
242        let var = makefile.variable_definitions().next().unwrap();
243        assert_eq!(var.assignment_operator(), Some(":=".to_string()));
244    }
245
246    #[test]
247    fn test_set_assignment_operator_simple_to_conditional() {
248        let makefile: Makefile = "VAR = value\n".parse().unwrap();
249        let mut var = makefile.variable_definitions().next().unwrap();
250        var.set_assignment_operator("?=");
251        assert_eq!(var.assignment_operator(), Some("?=".to_string()));
252        assert_eq!(makefile.code(), "VAR ?= value\n");
253    }
254
255    #[test]
256    fn test_set_assignment_operator_recursive_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_preserves_export() {
266        let makefile: Makefile = "export 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!(var.is_export());
271        assert_eq!(makefile.code(), "export VAR ?= value\n");
272    }
273
274    #[test]
275    fn test_set_assignment_operator_preserves_whitespace() {
276        let makefile: Makefile = "VAR  :=  value\n".parse().unwrap();
277        let mut var = makefile.variable_definitions().next().unwrap();
278        var.set_assignment_operator("?=");
279        assert_eq!(var.assignment_operator(), Some("?=".to_string()));
280        assert_eq!(makefile.code(), "VAR  ?=  value\n");
281    }
282
283    #[test]
284    fn test_set_assignment_operator_preserves_value() {
285        let makefile: Makefile = "VAR := old_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!(var.raw_value(), Some("old_value".to_string()));
290        assert_eq!(makefile.code(), "VAR = old_value\n");
291    }
292
293    #[test]
294    fn test_set_assignment_operator_to_triple_colon() {
295        let makefile: Makefile = "VAR := value\n".parse().unwrap();
296        let mut var = makefile.variable_definitions().next().unwrap();
297        var.set_assignment_operator("::=");
298        assert_eq!(var.assignment_operator(), Some("::=".to_string()));
299        assert_eq!(makefile.code(), "VAR ::= value\n");
300    }
301
302    #[test]
303    fn test_combined_operations() {
304        let makefile: Makefile = "export VAR := old_value\n".parse().unwrap();
305        let mut var = makefile.variable_definitions().next().unwrap();
306
307        // Change operator
308        var.set_assignment_operator("?=");
309        assert_eq!(var.assignment_operator(), Some("?=".to_string()));
310
311        // Change value
312        var.set_value("new_value");
313        assert_eq!(var.raw_value(), Some("new_value".to_string()));
314
315        // Verify everything
316        assert!(var.is_export());
317        assert_eq!(var.name(), Some("VAR".to_string()));
318        assert_eq!(makefile.code(), "export VAR ?= new_value\n");
319    }
320}