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 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 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 "block", "module",
267 "pass_statement", "break_statement", "continue_statement", "return_statement",
269 "expression_statement", "else_clause", "elif_clause", "if_clause", "for_in_clause",
270 "expression", "primary_expression", "parenthesized_expression",
272 "binary_operator", "boolean_operator", "comparison_operator",
273 "unary_operator", "not_operator",
274 "list_comprehension", "dictionary_comprehension",
276 "lambda", "lambda_parameters",
278 "identifier",
280 ];
281 validate_unused_kinds_audit(&Starlark, documented_unused)
282 .expect("Starlark unused node kinds audit failed");
283 }
284}