Skip to main content

normalize_languages/
starlark.rs

1//! Starlark (Bazel/Buck) support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Starlark language support.
9pub struct Starlark;
10
11impl Language for Starlark {
12    fn name(&self) -> &'static str {
13        "Starlark"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["star", "bzl"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "starlark"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &[]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["function_definition"]
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &[]
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &["load_statement"]
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["function_definition"]
44    }
45
46    fn visibility_mechanism(&self) -> VisibilityMechanism {
47        VisibilityMechanism::AllPublic
48    }
49
50    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51        if node.kind() != "function_definition" {
52            return Vec::new();
53        }
54
55        if let Some(name) = self.node_name(node, content) {
56            return vec![Export {
57                name: name.to_string(),
58                kind: SymbolKind::Function,
59                line: node.start_position().row + 1,
60            }];
61        }
62        Vec::new()
63    }
64
65    fn scope_creating_kinds(&self) -> &'static [&'static str] {
66        &["function_definition"]
67    }
68
69    fn control_flow_kinds(&self) -> &'static [&'static str] {
70        &["if_statement", "for_statement"]
71    }
72
73    fn complexity_nodes(&self) -> &'static [&'static str] {
74        &["if_statement", "for_statement", "conditional_expression"]
75    }
76
77    fn nesting_nodes(&self) -> &'static [&'static str] {
78        &["function_definition", "if_statement", "for_statement"]
79    }
80
81    fn signature_suffix(&self) -> &'static str {
82        ""
83    }
84
85    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
86        if node.kind() != "function_definition" {
87            return None;
88        }
89
90        let name = self.node_name(node, content)?;
91        let text = &content[node.byte_range()];
92        let first_line = text.lines().next().unwrap_or(text);
93
94        Some(Symbol {
95            name: name.to_string(),
96            kind: SymbolKind::Function,
97            signature: first_line.trim().to_string(),
98            docstring: None,
99            attributes: Vec::new(),
100            start_line: node.start_position().row + 1,
101            end_line: node.end_position().row + 1,
102            visibility: Visibility::Public,
103            children: Vec::new(),
104            is_interface_impl: false,
105            implements: Vec::new(),
106        })
107    }
108
109    fn extract_container(&self, _node: &Node, _content: &str) -> Option<Symbol> {
110        None
111    }
112    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
113        None
114    }
115    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
116        None
117    }
118
119    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
120        Vec::new()
121    }
122
123    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
124        if node.kind() != "load_statement" {
125            return Vec::new();
126        }
127
128        let text = &content[node.byte_range()];
129        vec![Import {
130            module: text.trim().to_string(),
131            names: Vec::new(),
132            alias: None,
133            is_wildcard: false,
134            is_relative: false,
135            line: node.start_position().row + 1,
136        }]
137    }
138
139    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
140        // Starlark: load("//path", "name")
141        let names_to_use: Vec<&str> = names
142            .map(|n| n.to_vec())
143            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
144        if names_to_use.is_empty() {
145            format!("load(\"{}\")", import.module)
146        } else {
147            let quoted: Vec<String> = names_to_use.iter().map(|n| format!("\"{}\"", n)).collect();
148            format!("load(\"{}\", {})", import.module, quoted.join(", "))
149        }
150    }
151
152    fn is_public(&self, _node: &Node, _content: &str) -> bool {
153        true
154    }
155    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
156        Visibility::Public
157    }
158
159    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
160        let name = symbol.name.as_str();
161        match symbol.kind {
162            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
163            crate::SymbolKind::Module => name == "tests" || name == "test",
164            _ => false,
165        }
166    }
167
168    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
169        None
170    }
171
172    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
173        node.child_by_field_name("body")
174    }
175
176    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
177        false
178    }
179
180    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
181        node.child_by_field_name("name")
182            .map(|n| &content[n.byte_range()])
183    }
184
185    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
186        let ext = path.extension()?.to_str()?;
187        if !["star", "bzl"].contains(&ext) {
188            return None;
189        }
190        let stem = path.file_stem()?.to_str()?;
191        Some(stem.to_string())
192    }
193
194    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
195        vec![format!("{}.star", module), format!("{}.bzl", module)]
196    }
197
198    fn lang_key(&self) -> &'static str {
199        "starlark"
200    }
201
202    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
203        false
204    }
205    fn find_stdlib(&self, _: &Path) -> Option<PathBuf> {
206        None
207    }
208    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
209        None
210    }
211    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
212        None
213    }
214    fn get_version(&self, _: &Path) -> Option<String> {
215        None
216    }
217    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
218        None
219    }
220    fn indexable_extensions(&self) -> &'static [&'static str] {
221        &["star", "bzl"]
222    }
223    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
224        Vec::new()
225    }
226
227    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
228        use crate::traits::{has_extension, skip_dotfiles};
229        if skip_dotfiles(name) {
230            return true;
231        }
232        !is_dir && !has_extension(name, self.indexable_extensions())
233    }
234
235    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
236        Vec::new()
237    }
238
239    fn package_module_name(&self, entry_name: &str) -> String {
240        entry_name
241            .strip_suffix(".star")
242            .or_else(|| entry_name.strip_suffix(".bzl"))
243            .unwrap_or(entry_name)
244            .to_string()
245    }
246
247    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
248        if path.is_file() {
249            Some(path.to_path_buf())
250        } else {
251            None
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::validate_unused_kinds_audit;
260
261    #[test]
262    fn unused_node_kinds_audit() {
263        #[rustfmt::skip]
264        let documented_unused: &[&str] = &[
265            // Blocks and modules
266            "block", "module",
267            // Statements
268            "pass_statement", "break_statement", "continue_statement", "return_statement",
269            "expression_statement", "else_clause", "elif_clause", "if_clause", "for_in_clause",
270            // Expressions
271            "expression", "primary_expression", "parenthesized_expression",
272            "binary_operator", "boolean_operator", "comparison_operator",
273            "unary_operator", "not_operator",
274            // Comprehensions
275            "list_comprehension", "dictionary_comprehension",
276            // Lambda
277            "lambda", "lambda_parameters",
278            // Other
279            "identifier",
280        ];
281        validate_unused_kinds_audit(&Starlark, documented_unused)
282            .expect("Starlark unused node kinds audit failed");
283    }
284}