1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Dockerfile;
10
11impl Language for Dockerfile {
12 fn name(&self) -> &'static str {
13 "Dockerfile"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["dockerfile"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "dockerfile"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
28 &["from_instruction"]
29 }
30
31 fn function_kinds(&self) -> &'static [&'static str] {
33 &[]
34 }
35
36 fn type_kinds(&self) -> &'static [&'static str] {
37 &[]
38 }
39
40 fn import_kinds(&self) -> &'static [&'static str] {
41 &["from_instruction"]
42 }
43
44 fn public_symbol_kinds(&self) -> &'static [&'static str] {
45 &["from_instruction"]
46 }
47
48 fn visibility_mechanism(&self) -> VisibilityMechanism {
49 VisibilityMechanism::NotApplicable
50 }
51
52 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
53 if node.kind() != "from_instruction" {
54 return Vec::new();
55 }
56
57 if let Some(name) = self.extract_stage_name(node, content) {
59 return vec![Export {
60 name,
61 kind: SymbolKind::Module,
62 line: node.start_position().row + 1,
63 }];
64 }
65
66 Vec::new()
67 }
68
69 fn scope_creating_kinds(&self) -> &'static [&'static str] {
70 &[]
71 }
72 fn control_flow_kinds(&self) -> &'static [&'static str] {
73 &[]
74 }
75 fn complexity_nodes(&self) -> &'static [&'static str] {
76 &[]
77 }
78 fn nesting_nodes(&self) -> &'static [&'static str] {
79 &[]
80 }
81
82 fn signature_suffix(&self) -> &'static str {
83 ""
84 }
85
86 fn extract_function(
87 &self,
88 _node: &Node,
89 _content: &str,
90 _in_container: bool,
91 ) -> Option<Symbol> {
92 None
93 }
94
95 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
96 if node.kind() != "from_instruction" {
97 return None;
98 }
99
100 let image_name = self.extract_image_name(node, content)?;
102 let stage_name = self.extract_stage_name(node, content);
103
104 let name = stage_name.clone().unwrap_or_else(|| image_name.clone());
105 let signature = if let Some(stage) = stage_name {
106 format!("FROM {} AS {}", image_name, stage)
107 } else {
108 format!("FROM {}", image_name)
109 };
110
111 Some(Symbol {
112 name,
113 kind: SymbolKind::Module,
114 signature,
115 docstring: None,
116 attributes: Vec::new(),
117 start_line: node.start_position().row + 1,
118 end_line: node.end_position().row + 1,
119 visibility: Visibility::Public,
120 children: Vec::new(),
121 is_interface_impl: false,
122 implements: Vec::new(),
123 })
124 }
125
126 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
127 None
128 }
129 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
130 None
131 }
132
133 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
134 Vec::new()
135 }
136
137 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
138 if node.kind() != "from_instruction" {
139 return Vec::new();
140 }
141
142 if let Some(image) = self.extract_image_name(node, content) {
143 return vec![Import {
144 module: image,
145 names: Vec::new(),
146 alias: self.extract_stage_name(node, content),
147 is_wildcard: false,
148 is_relative: false,
149 line: node.start_position().row + 1,
150 }];
151 }
152
153 Vec::new()
154 }
155
156 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
157 format!("FROM {}", import.module)
159 }
160
161 fn is_public(&self, _node: &Node, _content: &str) -> bool {
162 true
163 }
164 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
165 Visibility::Public
166 }
167
168 fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
169 false
170 }
171
172 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
173 None
174 }
175
176 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
177 None
178 }
179 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
180 false
181 }
182 fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
183 None
184 }
185
186 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
187 let name = path.file_name()?.to_str()?;
188 if name.to_lowercase() == "dockerfile" || name.ends_with(".dockerfile") {
189 Some(name.to_string())
190 } else {
191 None
192 }
193 }
194
195 fn module_name_to_paths(&self, _module: &str) -> Vec<String> {
196 vec!["Dockerfile".to_string()]
197 }
198
199 fn lang_key(&self) -> &'static str {
200 "dockerfile"
201 }
202
203 fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
204 false
205 }
206 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
207 None
208 }
209
210 fn resolve_local_import(
211 &self,
212 _import: &str,
213 _current_file: &Path,
214 _project_root: &Path,
215 ) -> Option<PathBuf> {
216 None
217 }
218
219 fn resolve_external_import(
220 &self,
221 _import_name: &str,
222 _project_root: &Path,
223 ) -> Option<ResolvedPackage> {
224 None
226 }
227
228 fn get_version(&self, _project_root: &Path) -> Option<String> {
229 None
230 }
231 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
232 None
233 }
234 fn indexable_extensions(&self) -> &'static [&'static str] {
235 &[]
236 }
237 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
238 Vec::new()
239 }
240
241 fn should_skip_package_entry(&self, name: &str, _is_dir: bool) -> bool {
242 use crate::traits::skip_dotfiles;
243 skip_dotfiles(name)
244 }
245
246 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
247 Vec::new()
248 }
249
250 fn package_module_name(&self, entry_name: &str) -> String {
251 entry_name.to_string()
252 }
253
254 fn find_package_entry(&self, _path: &Path) -> Option<PathBuf> {
255 None
256 }
257}
258
259impl Dockerfile {
260 fn extract_image_name(&self, node: &Node, content: &str) -> Option<String> {
262 let mut cursor = node.walk();
263 for child in node.children(&mut cursor) {
264 if child.kind() == "image_spec" {
265 return Some(content[child.byte_range()].to_string());
266 }
267 }
268 None
269 }
270
271 fn extract_stage_name(&self, node: &Node, content: &str) -> Option<String> {
273 let mut cursor = node.walk();
274 let mut found_as = false;
275 for child in node.children(&mut cursor) {
276 if found_as && child.kind() == "image_alias" {
277 return Some(content[child.byte_range()].to_string());
278 }
279 if child.kind() == "as_instruction" {
280 found_as = true;
281 }
282 }
283 None
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::validate_unused_kinds_audit;
291
292 #[test]
293 fn unused_node_kinds_audit() {
294 #[rustfmt::skip]
295 let documented_unused: &[&str] = &[
296 "add_instruction", "arg_instruction", "cmd_instruction", "copy_instruction",
298 "cross_build_instruction", "entrypoint_instruction", "env_instruction",
299 "expose_instruction", "healthcheck_instruction", "heredoc_block",
300 "label_instruction", "maintainer_instruction", "onbuild_instruction",
301 "run_instruction", "shell_instruction", "stopsignal_instruction",
302 "user_instruction", "volume_instruction", "workdir_instruction",
303 ];
304
305 validate_unused_kinds_audit(&Dockerfile, documented_unused)
306 .expect("Dockerfile unused node kinds audit failed");
307 }
308}