1use crate::external_packages::ResolvedPackage;
4use crate::{
5 Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism,
6 simple_function_symbol,
7};
8use std::path::{Path, PathBuf};
9use tree_sitter::Node;
10
11pub struct Hlsl;
13
14impl Language for Hlsl {
15 fn name(&self) -> &'static str {
16 "HLSL"
17 }
18 fn extensions(&self) -> &'static [&'static str] {
19 &["hlsl", "hlsli", "fx", "fxh", "cginc"]
20 }
21 fn grammar_name(&self) -> &'static str {
22 "hlsl"
23 }
24
25 fn has_symbols(&self) -> bool {
26 true
27 }
28
29 fn container_kinds(&self) -> &'static [&'static str] {
30 &["struct_specifier", "cbuffer_specifier"]
31 }
32
33 fn function_kinds(&self) -> &'static [&'static str] {
34 &["function_definition"]
35 }
36
37 fn type_kinds(&self) -> &'static [&'static str] {
38 &["struct_specifier"]
39 }
40
41 fn import_kinds(&self) -> &'static [&'static str] {
42 &["preproc_include"]
43 }
44
45 fn public_symbol_kinds(&self) -> &'static [&'static str] {
46 &["function_definition", "struct_specifier"]
47 }
48
49 fn visibility_mechanism(&self) -> VisibilityMechanism {
50 VisibilityMechanism::AllPublic
51 }
52
53 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
54 let name = match self.node_name(node, content) {
55 Some(n) => n.to_string(),
56 None => return Vec::new(),
57 };
58
59 let kind = match node.kind() {
60 "function_definition" => SymbolKind::Function,
61 "struct_specifier" => SymbolKind::Struct,
62 _ => return Vec::new(),
63 };
64
65 vec![Export {
66 name,
67 kind,
68 line: node.start_position().row + 1,
69 }]
70 }
71
72 fn scope_creating_kinds(&self) -> &'static [&'static str] {
73 &["function_definition", "compound_statement"]
74 }
75
76 fn control_flow_kinds(&self) -> &'static [&'static str] {
77 &[
78 "if_statement",
79 "for_statement",
80 "while_statement",
81 "switch_statement",
82 ]
83 }
84
85 fn complexity_nodes(&self) -> &'static [&'static str] {
86 &[
87 "if_statement",
88 "for_statement",
89 "while_statement",
90 "switch_statement",
91 "case_statement",
92 "conditional_expression",
93 ]
94 }
95
96 fn nesting_nodes(&self) -> &'static [&'static str] {
97 &[
98 "function_definition",
99 "if_statement",
100 "for_statement",
101 "while_statement",
102 ]
103 }
104
105 fn signature_suffix(&self) -> &'static str {
106 ""
107 }
108
109 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
110 let name = self.node_name(node, content)?;
111 Some(simple_function_symbol(
112 node,
113 content,
114 name,
115 self.extract_docstring(node, content),
116 ))
117 }
118
119 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
120 let name = self.node_name(node, content)?;
121
122 let kind = match node.kind() {
123 "struct_specifier" => SymbolKind::Struct,
124 "cbuffer_specifier" => SymbolKind::Module,
125 _ => return None,
126 };
127
128 let text = &content[node.byte_range()];
129 let first_line = text.lines().next().unwrap_or(text);
130
131 Some(Symbol {
132 name: name.to_string(),
133 kind,
134 signature: first_line.trim().to_string(),
135 docstring: self.extract_docstring(node, content),
136 attributes: Vec::new(),
137 start_line: node.start_position().row + 1,
138 end_line: node.end_position().row + 1,
139 visibility: Visibility::Public,
140 children: Vec::new(),
141 is_interface_impl: false,
142 implements: Vec::new(),
143 })
144 }
145
146 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
147 if node.kind() != "struct_specifier" {
148 return None;
149 }
150 self.extract_container(node, content)
151 }
152
153 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
154 let mut prev = node.prev_sibling();
156 while let Some(sibling) = prev {
157 let text = &content[sibling.byte_range()];
158 if sibling.kind() == "comment" {
159 if text.starts_with("/*") {
160 let inner = text.trim_start_matches("/*").trim_end_matches("*/").trim();
161 if !inner.is_empty() {
162 return Some(inner.lines().next().unwrap_or(inner).to_string());
163 }
164 } else if text.starts_with("//") {
165 let line = text.strip_prefix("//").unwrap_or(text).trim();
166 return Some(line.to_string());
167 }
168 }
169 prev = sibling.prev_sibling();
170 }
171 None
172 }
173
174 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
175 Vec::new()
176 }
177
178 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
179 if node.kind() != "preproc_include" {
180 return Vec::new();
181 }
182
183 let text = &content[node.byte_range()];
184 let line = node.start_position().row + 1;
185
186 let module = text
188 .split('"')
189 .nth(1)
190 .or_else(|| text.split('<').nth(1).and_then(|s| s.split('>').next()))
191 .map(|s| s.to_string());
192
193 if let Some(module) = module {
194 return vec![Import {
195 module,
196 names: Vec::new(),
197 alias: None,
198 is_wildcard: false,
199 is_relative: text.contains('"'),
200 line,
201 }];
202 }
203
204 Vec::new()
205 }
206
207 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
208 if import.is_relative {
210 format!("#include \"{}\"", import.module)
211 } else {
212 format!("#include <{}>", import.module)
213 }
214 }
215
216 fn is_public(&self, _node: &Node, _content: &str) -> bool {
217 true
218 }
219 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
220 Visibility::Public
221 }
222
223 fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
224 false
225 }
226
227 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
228 None
229 }
230
231 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
232 node.child_by_field_name("body")
233 }
234
235 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
236 false
237 }
238
239 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
240 node.child_by_field_name("declarator")
241 .and_then(|d| d.child_by_field_name("declarator"))
242 .map(|n| &content[n.byte_range()])
243 .or_else(|| {
244 node.child_by_field_name("name")
245 .map(|n| &content[n.byte_range()])
246 })
247 }
248
249 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
250 let ext = path.extension()?.to_str()?;
251 if !["hlsl", "hlsli", "fx", "fxh", "cginc"].contains(&ext) {
252 return None;
253 }
254 let stem = path.file_stem()?.to_str()?;
255 Some(stem.to_string())
256 }
257
258 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
259 vec![
260 format!("{}.hlsl", module),
261 format!("{}.hlsli", module),
262 format!("{}.fx", module),
263 ]
264 }
265
266 fn lang_key(&self) -> &'static str {
267 "hlsl"
268 }
269
270 fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
271 false
272 }
273 fn find_stdlib(&self, _: &Path) -> Option<PathBuf> {
274 None
275 }
276 fn resolve_local_import(&self, import: &str, current_file: &Path, _: &Path) -> Option<PathBuf> {
277 let dir = current_file.parent()?;
278 let full = dir.join(import);
279 if full.is_file() { Some(full) } else { None }
280 }
281 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
282 None
283 }
284 fn get_version(&self, _: &Path) -> Option<String> {
285 None
286 }
287 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
288 None
289 }
290 fn indexable_extensions(&self) -> &'static [&'static str] {
291 &["hlsl", "hlsli", "fx", "fxh", "cginc"]
292 }
293 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
294 Vec::new()
295 }
296
297 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
298 use crate::traits::{has_extension, skip_dotfiles};
299 if skip_dotfiles(name) {
300 return true;
301 }
302 !is_dir && !has_extension(name, self.indexable_extensions())
303 }
304
305 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
306 Vec::new()
307 }
308
309 fn package_module_name(&self, entry_name: &str) -> String {
310 for ext in &[".hlsl", ".hlsli", ".fx", ".fxh", ".cginc"] {
311 if let Some(name) = entry_name.strip_suffix(ext) {
312 return name.to_string();
313 }
314 }
315 entry_name.to_string()
316 }
317
318 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
319 if path.is_file() {
320 Some(path.to_path_buf())
321 } else {
322 None
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::validate_unused_kinds_audit;
331
332 #[test]
333 fn unused_node_kinds_audit() {
334 #[rustfmt::skip]
335 let documented_unused: &[&str] = &[
336 "abstract_function_declarator", "access_specifier", "alias_declaration",
337 "alignas_qualifier", "alignof_expression", "assignment_expression",
338 "attribute_declaration", "attribute_specifier", "attributed_statement",
339 "base_class_clause", "binary_expression", "bitfield_clause", "break_statement",
340 "call_expression", "cast_expression", "catch_clause",
341 "class_specifier", "co_await_expression", "co_return_statement", "co_yield_statement",
342 "comma_expression", "compound_literal_expression", "concept_definition",
343 "condition_clause", "consteval_block_declaration", "continue_statement",
344 "declaration", "declaration_list", "decltype", "default_method_clause",
345 "delete_expression", "delete_method_clause", "dependent_type", "destructor_name",
346 "discard_statement", "do_statement", "else_clause", "enum_specifier", "enumerator",
347 "enumerator_list", "expansion_statement", "explicit_function_specifier",
348 "explicit_object_parameter_declaration", "export_declaration", "expression_statement",
349 "extension_expression", "field_declaration", "field_declaration_list",
350 "field_expression", "field_identifier", "fold_expression", "for_range_loop",
351 "friend_declaration", "function_declarator", "generic_expression",
352 "global_module_fragment_declaration", "gnu_asm_expression", "gnu_asm_qualifier",
353 "goto_statement", "identifier", "import_declaration", "init_statement",
354 "labeled_statement", "lambda_capture_initializer", "lambda_capture_specifier",
355 "lambda_declarator", "lambda_default_capture", "lambda_expression", "lambda_specifier",
356 "linkage_specification", "module_declaration", "module_name", "module_partition",
357 "ms_based_modifier", "ms_call_modifier", "ms_declspec_modifier", "ms_pointer_modifier",
358 "ms_restrict_modifier", "ms_signed_ptr_modifier", "ms_unaligned_ptr_modifier",
359 "ms_unsigned_ptr_modifier", "namespace_alias_definition", "namespace_definition",
360 "namespace_identifier", "nested_namespace_specifier", "new_expression", "noexcept",
361 "offsetof_expression", "operator_cast", "operator_name", "optional_parameter_declaration",
362 "optional_type_parameter_declaration", "parameter_declaration", "parenthesized_expression",
363 "placeholder_type_specifier", "pointer_expression", "pointer_type_declarator",
364 "preproc_elif", "preproc_elifdef", "preproc_else", "preproc_function_def",
365 "preproc_if", "preproc_ifdef", "primitive_type", "private_module_fragment_declaration",
366 "pure_virtual_clause", "qualified_identifier", "qualifiers", "ref_qualifier",
367 "reflect_expression", "requires_clause", "requires_expression", "return_statement",
368 "seh_except_clause", "seh_finally_clause", "seh_leave_statement", "seh_try_statement",
369 "sized_type_specifier", "sizeof_expression", "splice_expression", "splice_specifier",
370 "splice_type_specifier", "statement_identifier", "static_assert_declaration",
371 "storage_class_specifier", "structured_binding_declarator", "subscript_expression",
372 "template_declaration", "template_function", "template_method",
373 "template_template_parameter_declaration", "template_type", "throw_specifier",
374 "throw_statement", "trailing_return_type", "try_statement", "type_definition",
375 "type_descriptor", "type_identifier", "type_parameter_declaration", "type_qualifier",
376 "type_requirement", "unary_expression", "union_specifier", "update_expression",
377 "using_declaration", "variadic_parameter_declaration",
378 "variadic_type_parameter_declaration", "virtual_specifier",
379 ];
380 validate_unused_kinds_audit(&Hlsl, documented_unused)
381 .expect("HLSL unused node kinds audit failed");
382 }
383}