Skip to main content

oak_dockerfile/builder/
mod.rs

1use crate::{DockerfileParser, ast::*, language::DockerfileLanguage, lexer::token_type::DockerfileTokenType, parser::element_type::DockerfileElementType};
2use oak_core::{Builder, BuilderCache, GreenNode, OakDiagnostics, OakError, Parser, RedNode, RedTree, SourceText, TextEdit, builder::BuildOutput, source::Source};
3
4/// AST builder for the Dockerfile language.
5#[derive(Clone, Copy)]
6pub struct DockerfileBuilder<'config> {
7    /// Language configuration.
8    config: &'config DockerfileLanguage,
9}
10
11impl<'config> DockerfileBuilder<'config> {
12    /// Creates a new `DockerfileBuilder` with the given language configuration.
13    pub fn new(config: &'config DockerfileLanguage) -> Self {
14        Self { config }
15    }
16}
17
18impl<'config> Builder<DockerfileLanguage> for DockerfileBuilder<'config> {
19    /// Builds the Dockerfile AST from the green tree.
20    fn build<'a, S: Source + ?Sized>(&self, source: &S, edits: &[TextEdit], cache: &'a mut impl BuilderCache<DockerfileLanguage>) -> BuildOutput<DockerfileLanguage> {
21        let parser = DockerfileParser::new(self.config);
22
23        let parse_result = parser.parse(source, edits, cache);
24
25        match parse_result.result {
26            Ok(green_tree) => {
27                let source_text = SourceText::new(source.get_text_in((0..source.length()).into()).into_owned());
28                match self.build_root(green_tree, &source_text) {
29                    Ok(ast_root) => OakDiagnostics { result: Ok(ast_root), diagnostics: parse_result.diagnostics },
30                    Err(build_error) => {
31                        let mut diagnostics = parse_result.diagnostics;
32                        diagnostics.push(build_error.clone());
33                        OakDiagnostics { result: Err(build_error), diagnostics }
34                    }
35                }
36            }
37            Err(parse_error) => OakDiagnostics { result: Err(parse_error), diagnostics: parse_result.diagnostics },
38        }
39    }
40}
41
42impl<'config> DockerfileBuilder<'config> {
43    /// Builds the AST root from the green tree.
44    pub(crate) fn build_root<'a>(&self, green_tree: &'a GreenNode<'a, DockerfileLanguage>, source: &SourceText) -> Result<DockerfileRoot, OakError> {
45        let root_node = RedNode::new(green_tree, 0);
46        let mut instructions = Vec::new();
47
48        for child in root_node.children() {
49            if let RedTree::Node(n) = child {
50                match n.green.kind {
51                    DockerfileElementType::From => instructions.push(self.build_from(n, source)?),
52                    DockerfileElementType::Run => instructions.push(self.build_run(n, source)?),
53                    DockerfileElementType::Copy => instructions.push(self.build_copy(n, source)?),
54                    DockerfileElementType::Add => instructions.push(self.build_add(n, source)?),
55                    DockerfileElementType::Workdir => instructions.push(self.build_workdir(n, source)?),
56                    DockerfileElementType::Expose => instructions.push(self.build_expose(n, source)?),
57                    DockerfileElementType::Env => instructions.push(self.build_env(n, source)?),
58                    DockerfileElementType::Cmd => instructions.push(self.build_cmd(n, source)?),
59                    DockerfileElementType::Entrypoint => instructions.push(self.build_entrypoint(n, source)?),
60                    DockerfileElementType::Volume => instructions.push(self.build_volume(n, source)?),
61                    DockerfileElementType::User => instructions.push(self.build_user(n, source)?),
62                    DockerfileElementType::Label => instructions.push(self.build_label(n, source)?),
63                    DockerfileElementType::Arg => instructions.push(self.build_arg(n, source)?),
64                    DockerfileElementType::Stopsignal => instructions.push(self.build_stopsignal(n, source)?),
65                    DockerfileElementType::Healthcheck => instructions.push(self.build_healthcheck(n, source)?),
66                    DockerfileElementType::Shell => instructions.push(self.build_shell(n, source)?),
67                    DockerfileElementType::Onbuild => instructions.push(self.build_onbuild(n, source)?),
68                    _ => {}
69                }
70            }
71        }
72
73        Ok(DockerfileRoot { instructions })
74    }
75
76    fn build_from(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
77        let mut image = String::new();
78        let mut children = node.children();
79
80        // Skip keyword
81        children.next();
82
83        for child in children {
84            if let RedTree::Leaf(t) = child {
85                if t.kind != DockerfileTokenType::Whitespace {
86                    image.push_str(source.get_text_in(t.span()).as_ref());
87                }
88            }
89        }
90
91        Ok(Instruction::From { image: image.trim().to_string(), tag: None, span: node.span() })
92    }
93
94    fn build_run(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
95        let mut command = String::new();
96        let mut children = node.children();
97
98        // Skip keyword
99        children.next();
100
101        for child in children {
102            if let RedTree::Leaf(t) = child {
103                if t.kind != DockerfileTokenType::Whitespace {
104                    command.push_str(source.get_text_in(t.span()).as_ref());
105                }
106            }
107        }
108
109        Ok(Instruction::Run { command: command.trim().to_string(), span: node.span() })
110    }
111
112    fn build_copy(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
113        let mut args = Vec::new();
114        let mut children = node.children();
115
116        // Skip keyword
117        children.next();
118
119        for child in children {
120            if let RedTree::Leaf(t) = child {
121                if t.kind != DockerfileTokenType::Whitespace {
122                    args.push(source.get_text_in(t.span()).to_string());
123                }
124            }
125        }
126
127        Ok(Instruction::Copy { src: args.get(0).cloned().unwrap_or_default(), dest: args.get(1).cloned().unwrap_or_default(), span: node.span() })
128    }
129
130    fn build_add(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
131        let mut args = Vec::new();
132        let mut children = node.children();
133
134        // Skip keyword
135        children.next();
136
137        for child in children {
138            if let RedTree::Leaf(t) = child {
139                if t.kind != DockerfileTokenType::Whitespace {
140                    args.push(source.get_text_in(t.span()).to_string());
141                }
142            }
143        }
144
145        Ok(Instruction::Add { src: args.get(0).cloned().unwrap_or_default(), dest: args.get(1).cloned().unwrap_or_default(), span: node.span() })
146    }
147
148    fn build_workdir(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
149        let mut path = String::new();
150        let mut children = node.children();
151        children.next(); // skip keyword
152        for child in children {
153            if let RedTree::Leaf(t) = child {
154                if t.kind != DockerfileTokenType::Whitespace {
155                    path.push_str(source.get_text_in(t.span()).as_ref());
156                }
157            }
158        }
159        Ok(Instruction::Workdir { path: path.trim().to_string(), span: node.span() })
160    }
161
162    fn build_expose(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
163        let mut port = String::new();
164        let mut children = node.children();
165        children.next(); // skip keyword
166        for child in children {
167            if let RedTree::Leaf(t) = child {
168                if t.kind != DockerfileTokenType::Whitespace {
169                    port.push_str(source.get_text_in(t.span()).as_ref());
170                }
171            }
172        }
173        Ok(Instruction::Expose { port: port.trim().to_string(), span: node.span() })
174    }
175
176    fn build_env(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
177        let mut key = String::new();
178        let mut value = String::new();
179        let mut children = node.children();
180        children.next(); // skip keyword
181
182        let mut parts = Vec::new();
183        for child in children {
184            if let RedTree::Leaf(t) = child {
185                if t.kind != DockerfileTokenType::Whitespace {
186                    parts.push(source.get_text_in(t.span()).to_string());
187                }
188            }
189        }
190
191        if parts.len() >= 2 {
192            key = parts[0].clone();
193            value = parts[1..].join(" ");
194        }
195        else if !parts.is_empty() {
196            key = parts[0].clone();
197        }
198
199        Ok(Instruction::Env { key, value, span: node.span() })
200    }
201
202    fn build_cmd(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
203        let mut command = String::new();
204        let mut children = node.children();
205        children.next(); // skip keyword
206        for child in children {
207            if let RedTree::Leaf(t) = child {
208                if t.kind != DockerfileTokenType::Whitespace {
209                    command.push_str(source.get_text_in(t.span()).as_ref());
210                }
211            }
212        }
213        Ok(Instruction::Cmd { command: command.trim().to_string(), span: node.span() })
214    }
215
216    fn build_entrypoint(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
217        let mut command = String::new();
218        let mut children = node.children();
219        children.next(); // skip keyword
220        for child in children {
221            if let RedTree::Leaf(t) = child {
222                if t.kind != DockerfileTokenType::Whitespace {
223                    command.push_str(source.get_text_in(t.span()).as_ref());
224                }
225            }
226        }
227        Ok(Instruction::Entrypoint { command: command.trim().to_string(), span: node.span() })
228    }
229
230    fn build_volume(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
231        let mut path = String::new();
232        let mut children = node.children();
233        children.next(); // skip keyword
234        for child in children {
235            if let RedTree::Leaf(t) = child {
236                if t.kind != DockerfileTokenType::Whitespace {
237                    path.push_str(source.get_text_in(t.span()).as_ref());
238                }
239            }
240        }
241        Ok(Instruction::Volume { path: path.trim().to_string(), span: node.span() })
242    }
243
244    fn build_user(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
245        let mut user = String::new();
246        let mut children = node.children();
247        children.next(); // skip keyword
248        for child in children {
249            if let RedTree::Leaf(t) = child {
250                if t.kind != DockerfileTokenType::Whitespace {
251                    user.push_str(source.get_text_in(t.span()).as_ref());
252                }
253            }
254        }
255        Ok(Instruction::User { user: user.trim().to_string(), span: node.span() })
256    }
257
258    fn build_label(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
259        let mut key = String::new();
260        let mut value = String::new();
261        let mut children = node.children();
262        children.next(); // skip keyword
263
264        let mut parts = Vec::new();
265        for child in children {
266            if let RedTree::Leaf(t) = child {
267                if t.kind != DockerfileTokenType::Whitespace {
268                    parts.push(source.get_text_in(t.span()).to_string());
269                }
270            }
271        }
272
273        if parts.len() >= 2 {
274            key = parts[0].clone();
275            value = parts[1..].join(" ");
276        }
277        else if !parts.is_empty() {
278            key = parts[0].clone();
279        }
280
281        Ok(Instruction::Label { key, value, span: node.span() })
282    }
283
284    fn build_arg(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
285        let mut name = String::new();
286        let mut default = None;
287        let mut children = node.children();
288        children.next(); // skip keyword
289
290        let mut parts = Vec::new();
291        for child in children {
292            if let RedTree::Leaf(t) = child {
293                if t.kind != DockerfileTokenType::Whitespace {
294                    parts.push(source.get_text_in(t.span()).to_string());
295                }
296            }
297        }
298
299        if !parts.is_empty() {
300            name = parts[0].clone();
301            if parts.len() > 1 {
302                default = Some(parts[1..].join(" "));
303            }
304        }
305
306        Ok(Instruction::Arg { name, default, span: node.span() })
307    }
308
309    fn build_stopsignal(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
310        let mut signal = String::new();
311        let mut children = node.children();
312        children.next(); // skip keyword
313        for child in children {
314            if let RedTree::Leaf(t) = child {
315                if t.kind != DockerfileTokenType::Whitespace {
316                    signal.push_str(source.get_text_in(t.span()).as_ref());
317                }
318            }
319        }
320        Ok(Instruction::Stopsignal { signal: signal.trim().to_string(), span: node.span() })
321    }
322
323    fn build_healthcheck(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
324        let mut command = String::new();
325        let mut children = node.children();
326        children.next(); // skip keyword
327        for child in children {
328            if let RedTree::Leaf(t) = child {
329                if t.kind != DockerfileTokenType::Whitespace {
330                    command.push_str(source.get_text_in(t.span()).as_ref());
331                }
332            }
333        }
334        Ok(Instruction::Healthcheck { command: command.trim().to_string(), span: node.span() })
335    }
336
337    fn build_shell(&self, node: RedNode<DockerfileLanguage>, source: &SourceText) -> Result<Instruction, OakError> {
338        let mut shell = String::new();
339        let mut children = node.children();
340        children.next(); // skip keyword
341        for child in children {
342            if let RedTree::Leaf(t) = child {
343                if t.kind != DockerfileTokenType::Whitespace {
344                    shell.push_str(source.get_text_in(t.span()).as_ref());
345                }
346            }
347        }
348        Ok(Instruction::Shell { shell: shell.trim().to_string(), span: node.span() })
349    }
350
351    fn build_onbuild(&self, node: RedNode<DockerfileLanguage>, _source: &SourceText) -> Result<Instruction, OakError> {
352        // Simple implementation for now
353        Ok(Instruction::Onbuild { instruction: Box::new(Instruction::Run { command: "ONBUILD placeholder".to_string(), span: node.span() }), span: node.span() })
354    }
355}
356
357fn text(source: &SourceText, span: core::range::Range<usize>) -> String {
358    source.get_text_in(span).to_string()
359}