1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use tree_sitter::Node;
8
9pub fn get_rust_version() -> Option<String> {
15 let output = Command::new("rustc").args(["--version"]).output().ok()?;
16
17 if output.status.success() {
18 let version_str = String::from_utf8_lossy(&output.stdout);
19 for part in version_str.split_whitespace() {
21 if part.chars().next().is_some_and(|c| c.is_ascii_digit()) {
22 let parts: Vec<&str> = part.split('.').collect();
23 if parts.len() >= 2 {
24 return Some(format!("{}.{}", parts[0], parts[1]));
25 }
26 }
27 }
28 }
29
30 None
31}
32
33pub fn find_cargo_registry() -> Option<PathBuf> {
36 if let Ok(cargo_home) = std::env::var("CARGO_HOME") {
38 let registry = PathBuf::from(cargo_home).join("registry").join("src");
39 if registry.is_dir() {
40 return Some(registry);
41 }
42 }
43
44 if let Ok(home) = std::env::var("HOME") {
46 let registry = PathBuf::from(home)
47 .join(".cargo")
48 .join("registry")
49 .join("src");
50 if registry.is_dir() {
51 return Some(registry);
52 }
53 }
54
55 if let Ok(home) = std::env::var("USERPROFILE") {
57 let registry = PathBuf::from(home)
58 .join(".cargo")
59 .join("registry")
60 .join("src");
61 if registry.is_dir() {
62 return Some(registry);
63 }
64 }
65
66 None
67}
68
69fn resolve_rust_crate(crate_name: &str, registry: &Path) -> Option<ResolvedPackage> {
71 if let Ok(indices) = std::fs::read_dir(registry) {
73 for index_entry in indices.flatten() {
74 let index_path = index_entry.path();
75 if !index_path.is_dir() {
76 continue;
77 }
78
79 if let Ok(crates) = std::fs::read_dir(&index_path) {
80 for crate_entry in crates.flatten() {
81 let crate_dir = crate_entry.path();
82 let dir_name = crate_entry.file_name().to_string_lossy().to_string();
83
84 if dir_name.starts_with(&format!("{}-", crate_name)) {
86 let lib_rs = crate_dir.join("src").join("lib.rs");
87 if lib_rs.is_file() {
88 return Some(ResolvedPackage {
89 path: lib_rs,
90 name: crate_name.to_string(),
91 is_namespace: false,
92 });
93 }
94 }
95 }
96 }
97 }
98 }
99
100 None
101}
102
103pub struct Rust;
109
110impl Language for Rust {
111 fn name(&self) -> &'static str {
112 "Rust"
113 }
114 fn extensions(&self) -> &'static [&'static str] {
115 &["rs"]
116 }
117 fn grammar_name(&self) -> &'static str {
118 "rust"
119 }
120
121 fn has_symbols(&self) -> bool {
122 true
123 }
124
125 fn container_kinds(&self) -> &'static [&'static str] {
126 &["impl_item", "trait_item", "mod_item"]
127 }
128
129 fn function_kinds(&self) -> &'static [&'static str] {
130 &["function_item"]
131 }
132
133 fn type_kinds(&self) -> &'static [&'static str] {
134 &["struct_item", "enum_item", "type_item", "trait_item"]
135 }
136
137 fn import_kinds(&self) -> &'static [&'static str] {
138 &["use_declaration"]
139 }
140
141 fn public_symbol_kinds(&self) -> &'static [&'static str] {
142 &["function_item", "struct_item", "enum_item", "trait_item"]
143 }
144
145 fn visibility_mechanism(&self) -> VisibilityMechanism {
146 VisibilityMechanism::AccessModifier
147 }
148
149 fn complexity_nodes(&self) -> &'static [&'static str] {
150 &[
151 "if_expression",
152 "match_expression",
153 "for_expression",
154 "while_expression",
155 "loop_expression",
156 "match_arm",
157 "binary_expression", ]
159 }
160
161 fn nesting_nodes(&self) -> &'static [&'static str] {
162 &[
163 "if_expression",
164 "match_expression",
165 "for_expression",
166 "while_expression",
167 "loop_expression",
168 "function_item",
169 "impl_item",
170 "trait_item",
171 "mod_item",
172 ]
173 }
174
175 fn signature_suffix(&self) -> &'static str {
176 " {}"
177 }
178
179 fn scope_creating_kinds(&self) -> &'static [&'static str] {
180 &[
182 "block",
183 "for_expression",
184 "while_expression",
185 "loop_expression",
186 "closure_expression",
187 ]
188 }
189
190 fn control_flow_kinds(&self) -> &'static [&'static str] {
191 &[
192 "if_expression",
193 "match_expression",
194 "for_expression",
195 "while_expression",
196 "loop_expression",
197 "return_expression",
198 "break_expression",
199 "continue_expression",
200 ]
201 }
202
203 fn extract_function(&self, node: &Node, content: &str, in_container: bool) -> Option<Symbol> {
204 let name = self.node_name(node, content)?;
205
206 let mut vis = String::new();
208 let mut cursor = node.walk();
209 for child in node.children(&mut cursor) {
210 if child.kind() == "visibility_modifier" {
211 vis = format!("{} ", &content[child.byte_range()]);
212 break;
213 }
214 }
215
216 let params = node
217 .child_by_field_name("parameters")
218 .map(|p| content[p.byte_range()].to_string())
219 .unwrap_or_else(|| "()".to_string());
220
221 let return_type = node
222 .child_by_field_name("return_type")
223 .map(|r| format!(" -> {}", &content[r.byte_range()]))
224 .unwrap_or_default();
225
226 let signature = format!("{}fn {}{}{}", vis, name, params, return_type);
227
228 Some(Symbol {
229 name: name.to_string(),
230 kind: if in_container {
231 SymbolKind::Method
232 } else {
233 SymbolKind::Function
234 },
235 signature,
236 docstring: self.extract_docstring(node, content),
237 attributes: self.extract_attributes(node, content),
238 start_line: node.start_position().row + 1,
239 end_line: node.end_position().row + 1,
240 visibility: self.get_visibility(node, content),
241 children: Vec::new(),
242 is_interface_impl: false,
243 implements: Vec::new(),
244 })
245 }
246
247 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
248 match node.kind() {
249 "impl_item" => {
250 let type_node = node.child_by_field_name("type")?;
251 let type_name = &content[type_node.byte_range()];
252
253 let is_trait_impl = node.child_by_field_name("trait").is_some();
255
256 let signature = if let Some(trait_node) = node.child_by_field_name("trait") {
257 let trait_name = &content[trait_node.byte_range()];
258 format!("impl {} for {}", trait_name, type_name)
259 } else {
260 format!("impl {}", type_name)
261 };
262
263 Some(Symbol {
264 name: type_name.to_string(),
265 kind: SymbolKind::Module, signature,
267 docstring: None,
268 attributes: self.extract_attributes(node, content),
269 start_line: node.start_position().row + 1,
270 end_line: node.end_position().row + 1,
271 visibility: Visibility::Public,
272 children: Vec::new(),
273 is_interface_impl: is_trait_impl,
274 implements: Vec::new(),
275 })
276 }
277 "trait_item" => {
278 let name = self.node_name(node, content)?;
279 let vis = self.extract_visibility_prefix(node, content);
280
281 Some(Symbol {
282 name: name.to_string(),
283 kind: SymbolKind::Trait,
284 signature: format!("{}trait {}", vis, name),
285 docstring: self.extract_docstring(node, content),
286 attributes: self.extract_attributes(node, content),
287 start_line: node.start_position().row + 1,
288 end_line: node.end_position().row + 1,
289 visibility: self.get_visibility(node, content),
290 children: Vec::new(),
291 is_interface_impl: false,
292 implements: Vec::new(),
293 })
294 }
295 "mod_item" => {
296 node.child_by_field_name("body")?;
298 let name = self.node_name(node, content)?;
299 let vis = self.extract_visibility_prefix(node, content);
300
301 Some(Symbol {
302 name: name.to_string(),
303 kind: SymbolKind::Module,
304 signature: format!("{}mod {}", vis, name),
305 docstring: self.extract_docstring(node, content),
306 attributes: self.extract_attributes(node, content),
307 start_line: node.start_position().row + 1,
308 end_line: node.end_position().row + 1,
309 visibility: self.get_visibility(node, content),
310 children: Vec::new(),
311 is_interface_impl: false,
312 implements: Vec::new(),
313 })
314 }
315 _ => None,
316 }
317 }
318
319 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
320 let name = self.node_name(node, content)?;
321 let vis = self.extract_visibility_prefix(node, content);
322
323 let (kind, keyword) = match node.kind() {
324 "struct_item" => (SymbolKind::Struct, "struct"),
325 "enum_item" => (SymbolKind::Enum, "enum"),
326 "type_item" => (SymbolKind::Type, "type"),
327 "trait_item" => (SymbolKind::Trait, "trait"),
328 _ => return None,
329 };
330
331 Some(Symbol {
332 name: name.to_string(),
333 kind,
334 signature: format!("{}{} {}", vis, keyword, name),
335 docstring: self.extract_docstring(node, content),
336 attributes: self.extract_attributes(node, content),
337 start_line: node.start_position().row + 1,
338 end_line: node.end_position().row + 1,
339 visibility: self.get_visibility(node, content),
340 children: Vec::new(),
341 is_interface_impl: false,
342 implements: Vec::new(),
343 })
344 }
345
346 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
347 let mut cursor = node.walk();
349 for child in node.children(&mut cursor) {
350 if child.kind() == "attributes" {
351 let mut doc_lines = Vec::new();
352 let mut attr_cursor = child.walk();
353 for attr_child in child.children(&mut attr_cursor) {
354 if attr_child.kind() == "line_outer_doc_comment" {
355 let text = &content[attr_child.byte_range()];
356 let doc = text.trim_start_matches("///").trim();
357 if !doc.is_empty() {
358 doc_lines.push(doc.to_string());
359 }
360 }
361 }
362 if !doc_lines.is_empty() {
363 return Some(doc_lines.join("\n"));
364 }
365 }
366 }
367 None
368 }
369
370 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
371 let mut attrs = Vec::new();
372
373 if let Some(attr_node) = node.child_by_field_name("attributes") {
375 let mut cursor = attr_node.walk();
376 for child in attr_node.children(&mut cursor) {
377 if child.kind() == "attribute_item" {
378 attrs.push(content[child.byte_range()].to_string());
379 }
380 }
381 }
382
383 let mut prev = node.prev_sibling();
385 while let Some(sibling) = prev {
386 if sibling.kind() == "attribute_item" {
387 attrs.insert(0, content[sibling.byte_range()].to_string());
389 prev = sibling.prev_sibling();
390 } else {
391 break;
392 }
393 }
394
395 attrs
396 }
397
398 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
399 if node.kind() != "use_declaration" {
400 return Vec::new();
401 }
402
403 let line = node.start_position().row + 1;
404 let text = &content[node.byte_range()];
405 let module = text.trim_start_matches("use ").trim_end_matches(';').trim();
406
407 let mut names = Vec::new();
409 let is_relative = module.starts_with("crate")
410 || module.starts_with("self")
411 || module.starts_with("super");
412
413 if let Some(brace_start) = module.find('{') {
414 let prefix = module[..brace_start].trim_end_matches("::");
415 if let Some(brace_end) = module.find('}') {
416 let items = &module[brace_start + 1..brace_end];
417 for item in items.split(',') {
418 let trimmed = item.trim();
419 if !trimmed.is_empty() {
420 names.push(trimmed.to_string());
421 }
422 }
423 }
424 vec![Import {
425 module: prefix.to_string(),
426 names,
427 alias: None,
428 is_wildcard: false,
429 is_relative,
430 line,
431 }]
432 } else {
433 let (module_part, alias) = if let Some(as_pos) = module.find(" as ") {
435 (&module[..as_pos], Some(module[as_pos + 4..].to_string()))
436 } else {
437 (module, None)
438 };
439
440 vec![Import {
441 module: module_part.to_string(),
442 names: Vec::new(),
443 alias,
444 is_wildcard: module_part.ends_with("::*"),
445 is_relative,
446 line,
447 }]
448 }
449 }
450
451 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
452 let names_to_use: Vec<&str> = names
453 .map(|n| n.to_vec())
454 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
455
456 if import.is_wildcard {
457 format!("use {};", import.module)
459 } else if names_to_use.is_empty() {
460 format!("use {};", import.module)
461 } else if names_to_use.len() == 1 {
462 format!("use {}::{};", import.module, names_to_use[0])
463 } else {
464 format!("use {}::{{{}}};", import.module, names_to_use.join(", "))
465 }
466 }
467
468 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
469 let line = node.start_position().row + 1;
470
471 if !self.is_public(node, content) {
473 return Vec::new();
474 }
475
476 let name = match self.node_name(node, content) {
477 Some(n) => n.to_string(),
478 None => return Vec::new(),
479 };
480
481 let kind = match node.kind() {
482 "function_item" => SymbolKind::Function,
483 "struct_item" => SymbolKind::Struct,
484 "enum_item" => SymbolKind::Enum,
485 "trait_item" => SymbolKind::Trait,
486 _ => return Vec::new(),
487 };
488
489 vec![Export { name, kind, line }]
490 }
491
492 fn is_public(&self, node: &Node, content: &str) -> bool {
493 let mut cursor = node.walk();
494 for child in node.children(&mut cursor) {
495 if child.kind() == "visibility_modifier" {
496 let vis = &content[child.byte_range()];
497 return vis.starts_with("pub");
498 }
499 }
500 false
501 }
502
503 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
504 let mut cursor = node.walk();
505 for child in node.children(&mut cursor) {
506 if child.kind() == "visibility_modifier" {
507 let vis = &content[child.byte_range()];
508 if vis == "pub" {
509 return Visibility::Public;
510 } else if vis.starts_with("pub(crate)") {
511 return Visibility::Internal;
512 } else if vis.starts_with("pub(super)") || vis.starts_with("pub(in") {
513 return Visibility::Protected;
514 }
515 }
516 }
517 Visibility::Private
518 }
519
520 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
521 let in_attrs = symbol
522 .attributes
523 .iter()
524 .any(|a| a.contains("#[test]") || a.contains("#[cfg(test)]"));
525 let in_sig =
526 symbol.signature.contains("#[test]") || symbol.signature.contains("#[cfg(test)]");
527 if in_attrs || in_sig {
528 return true;
529 }
530 match symbol.kind {
531 crate::SymbolKind::Function | crate::SymbolKind::Method => {
532 symbol.name.starts_with("test_")
533 }
534 crate::SymbolKind::Module => symbol.name == "tests",
535 _ => false,
536 }
537 }
538
539 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
540 None
541 }
542
543 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
544 node.child_by_field_name("body")
545 }
546
547 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
548 false
550 }
551
552 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
553 let name_node = node.child_by_field_name("name")?;
554 Some(&content[name_node.byte_range()])
555 }
556
557 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
558 if path.extension()?.to_str()? != "rs" {
560 return None;
561 }
562
563 let path_str = path.to_str()?;
564
565 let rel_path = path_str.strip_prefix("src/").unwrap_or(path_str);
567
568 let module_path = rel_path.strip_suffix(".rs")?;
570
571 let module_path = if module_path.ends_with("/mod") || module_path.ends_with("/lib") {
573 module_path.rsplit_once('/')?.0
574 } else {
575 module_path
576 };
577
578 Some(module_path.replace('/', "::"))
580 }
581
582 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
583 let rel_path = module.replace("::", "/");
584
585 vec![
586 format!("src/{}.rs", rel_path),
587 format!("src/{}/mod.rs", rel_path),
588 ]
589 }
590
591 fn lang_key(&self) -> &'static str {
594 "rust"
595 }
596
597 fn resolve_local_import(
598 &self,
599 module: &str,
600 current_file: &Path,
601 project_root: &Path,
602 ) -> Option<PathBuf> {
603 let crate_root = find_crate_root(current_file, project_root)?;
605
606 if module.starts_with("crate::") {
607 let path_part = module.strip_prefix("crate::")?.replace("::", "/");
609 let src_dir = crate_root.join("src");
610
611 let direct = src_dir.join(format!("{}.rs", path_part));
613 if direct.exists() {
614 return Some(direct);
615 }
616
617 let mod_file = src_dir.join(&path_part).join("mod.rs");
619 if mod_file.exists() {
620 return Some(mod_file);
621 }
622 } else if module.starts_with("super::") {
623 let current_dir = current_file.parent()?;
625 let parent_dir = current_dir.parent()?;
626 let path_part = module.strip_prefix("super::")?.replace("::", "/");
627
628 let direct = parent_dir.join(format!("{}.rs", path_part));
630 if direct.exists() {
631 return Some(direct);
632 }
633
634 let mod_file = parent_dir.join(&path_part).join("mod.rs");
636 if mod_file.exists() {
637 return Some(mod_file);
638 }
639 } else if module.starts_with("self::") {
640 let current_dir = current_file.parent()?;
642 let path_part = module.strip_prefix("self::")?.replace("::", "/");
643
644 let direct = current_dir.join(format!("{}.rs", path_part));
646 if direct.exists() {
647 return Some(direct);
648 }
649
650 let mod_file = current_dir.join(&path_part).join("mod.rs");
652 if mod_file.exists() {
653 return Some(mod_file);
654 }
655 }
656
657 None
658 }
659
660 fn resolve_external_import(
661 &self,
662 crate_name: &str,
663 _project_root: &Path,
664 ) -> Option<ResolvedPackage> {
665 let registry = find_cargo_registry()?;
666 resolve_rust_crate(crate_name, ®istry)
667 }
668
669 fn get_version(&self, _project_root: &Path) -> Option<String> {
670 get_rust_version()
671 }
672
673 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
674 find_cargo_registry()
675 }
676
677 fn indexable_extensions(&self) -> &'static [&'static str] {
678 &["rs"]
679 }
680
681 fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
682 false
684 }
685
686 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
687 None
689 }
690
691 fn package_sources(&self, project_root: &Path) -> Vec<crate::PackageSource> {
692 use crate::{PackageSource, PackageSourceKind};
693 let mut sources = Vec::new();
694 if let Some(cache) = self.find_package_cache(project_root) {
695 sources.push(PackageSource {
696 name: "cargo-registry",
697 path: cache,
698 kind: PackageSourceKind::Cargo,
699 version_specific: false,
700 });
701 }
702 sources
703 }
704
705 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
706 use crate::traits::{has_extension, skip_dotfiles};
707 if skip_dotfiles(name) {
708 return true;
709 }
710 if is_dir
712 && (name == "target" || name == "tests" || name == "benches" || name == "examples")
713 {
714 return true;
715 }
716 !is_dir && !has_extension(name, self.indexable_extensions())
718 }
719
720 fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
721 if source.kind != crate::PackageSourceKind::Cargo {
722 return Vec::new();
723 }
724 discover_cargo_packages(&source.path)
725 }
726
727 fn package_module_name(&self, entry_name: &str) -> String {
728 entry_name
730 .strip_suffix(".rs")
731 .unwrap_or(entry_name)
732 .to_string()
733 }
734
735 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
736 if path.is_file() {
737 return Some(path.to_path_buf());
738 }
739 let lib_rs = path.join("src").join("lib.rs");
741 if lib_rs.is_file() {
742 return Some(lib_rs);
743 }
744 let mod_rs = path.join("mod.rs");
746 if mod_rs.is_file() {
747 return Some(mod_rs);
748 }
749 None
750 }
751}
752
753fn discover_cargo_packages(registry: &Path) -> Vec<(String, PathBuf)> {
756 let mut packages = Vec::new();
757
758 let indices = match std::fs::read_dir(registry) {
760 Ok(e) => e,
761 Err(_) => return packages,
762 };
763
764 for index_entry in indices.flatten() {
765 let index_path = index_entry.path();
766 if !index_path.is_dir() {
767 continue;
768 }
769
770 let crates = match std::fs::read_dir(&index_path) {
771 Ok(e) => e,
772 Err(_) => continue,
773 };
774
775 for crate_entry in crates.flatten() {
776 let crate_path = crate_entry.path();
777 let crate_name = crate_entry.file_name().to_string_lossy().to_string();
778
779 if !crate_path.is_dir() {
780 continue;
781 }
782
783 let name = crate_name
785 .rsplit_once('-')
786 .map(|(n, _)| n)
787 .unwrap_or(&crate_name);
788
789 let lib_rs = crate_path.join("src").join("lib.rs");
791 if lib_rs.is_file() {
792 packages.push((name.to_string(), lib_rs));
793 }
794 }
795 }
796
797 packages
798}
799
800fn find_crate_root(start: &Path, root: &Path) -> Option<PathBuf> {
802 let mut current = start.parent()?;
803 while current.starts_with(root) {
804 if current.join("Cargo.toml").exists() {
805 return Some(current.to_path_buf());
806 }
807 current = current.parent()?;
808 }
809 None
810}
811
812impl Rust {
813 fn extract_visibility_prefix(&self, node: &Node, content: &str) -> String {
814 let mut cursor = node.walk();
815 for child in node.children(&mut cursor) {
816 if child.kind() == "visibility_modifier" {
817 return format!("{} ", &content[child.byte_range()]);
818 }
819 }
820 String::new()
821 }
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use crate::validate_unused_kinds_audit;
828
829 #[test]
832 fn unused_node_kinds_audit() {
833 #[rustfmt::skip]
844 let documented_unused: &[&str] = &[
845 "block_comment", "declaration_list", "field_declaration", "field_declaration_list", "field_expression", "field_identifier", "identifier", "lifetime", "lifetime_parameter", "ordered_field_declaration_list", "scoped_identifier", "scoped_type_identifier", "shorthand_field_identifier", "type_identifier", "visibility_modifier", "else_clause", "enum_variant", "enum_variant_list", "match_block", "match_pattern", "trait_bounds", "where_clause", "array_expression", "assignment_expression", "async_block", "await_expression", "call_expression", "generic_function", "index_expression", "parenthesized_expression","range_expression", "reference_expression", "struct_expression", "try_expression", "tuple_expression", "type_cast_expression", "unary_expression", "unit_expression", "yield_expression", "abstract_type", "array_type", "bounded_type", "bracketed_type", "dynamic_type", "function_type", "generic_type", "generic_type_with_turbofish", "higher_ranked_trait_bound", "never_type", "pointer_type", "primitive_type", "qualified_type", "reference_type", "removed_trait_bound", "tuple_type", "type_arguments", "type_binding", "type_parameter", "type_parameters", "unit_type", "unsafe_bound_type", "block_outer_doc_comment", "extern_modifier", "function_modifiers", "mutable_specifier", "struct_pattern", "tuple_struct_pattern", "fragment_specifier", "macro_arguments_declaration", "macro_body_v2", "macro_definition", "macro_definition_v2", "block_expression_with_attribute", "const_block", "expression_statement", "expression_with_attribute", "extern_crate_declaration","foreign_mod_item", "function_signature_item", "gen_block", "let_declaration", "try_block", "unsafe_block", "use_as_clause", "empty_statement", ];
946
947 validate_unused_kinds_audit(&Rust, documented_unused)
948 .expect("Rust unused node kinds audit failed");
949 }
950}