Skip to main content

normalize_languages/
dockerfile.rs

1//! Dockerfile language support.
2
3use crate::{Import, Language, LanguageSymbols};
4use tree_sitter::Node;
5
6/// Dockerfile language support.
7pub struct Dockerfile;
8
9impl Language for Dockerfile {
10    fn name(&self) -> &'static str {
11        "Dockerfile"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["dockerfile"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "dockerfile"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    // Dockerfiles have stages (FROM ... AS name) that act as containers
25
26    // No functions in Dockerfile
27
28    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
29        if node.kind() != "from_instruction" {
30            return Vec::new();
31        }
32
33        if let Some(image) = self.extract_image_name(node, content) {
34            return vec![Import {
35                module: image,
36                names: Vec::new(),
37                alias: self.extract_stage_name(node, content),
38                is_wildcard: false,
39                is_relative: false,
40                line: node.start_position().row + 1,
41            }];
42        }
43
44        Vec::new()
45    }
46
47    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
48        // Dockerfile: FROM image
49        format!("FROM {}", import.module)
50    }
51
52    fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
53        None
54    }
55}
56
57impl LanguageSymbols for Dockerfile {}
58
59impl Dockerfile {
60    /// Extract the image name from a FROM instruction
61    fn extract_image_name(&self, node: &Node, content: &str) -> Option<String> {
62        let mut cursor = node.walk();
63        for child in node.children(&mut cursor) {
64            if child.kind() == "image_spec" {
65                return Some(content[child.byte_range()].to_string());
66            }
67        }
68        None
69    }
70
71    /// Extract the stage name from a FROM instruction (FROM image AS name)
72    fn extract_stage_name(&self, node: &Node, content: &str) -> Option<String> {
73        let mut cursor = node.walk();
74        let mut found_as = false;
75        for child in node.children(&mut cursor) {
76            if found_as && child.kind() == "image_alias" {
77                return Some(content[child.byte_range()].to_string());
78            }
79            if child.kind() == "as_instruction" {
80                found_as = true;
81            }
82        }
83        None
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::validate_unused_kinds_audit;
91
92    #[test]
93    fn unused_node_kinds_audit() {
94        #[rustfmt::skip]
95        let documented_unused: &[&str] = &[
96            // Dockerfile instruction types not tracked as symbols
97            "add_instruction", "cmd_instruction", "copy_instruction",
98            "cross_build_instruction", "entrypoint_instruction",
99            "expose_instruction", "healthcheck_instruction", "heredoc_block",
100            "label_instruction", "maintainer_instruction", "onbuild_instruction",
101            "run_instruction", "shell_instruction", "stopsignal_instruction",
102            "user_instruction", "volume_instruction", "workdir_instruction",
103        ];
104
105        validate_unused_kinds_audit(&Dockerfile, documented_unused)
106            .expect("Dockerfile unused node kinds audit failed");
107    }
108}