Skip to main content

normalize_languages/
rust.rs

1//! Rust language support.
2
3use std::path::{Path, PathBuf};
4
5use crate::{
6    ContainerBody, Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver,
7    Resolution, ResolverConfig, Visibility,
8};
9use tree_sitter::Node;
10
11/// Rust language support.
12pub struct Rust;
13
14impl Language for Rust {
15    fn name(&self) -> &'static str {
16        "Rust"
17    }
18    fn extensions(&self) -> &'static [&'static str] {
19        &["rs"]
20    }
21    fn grammar_name(&self) -> &'static str {
22        "rust"
23    }
24
25    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
26        Some(self)
27    }
28
29    fn signature_suffix(&self) -> &'static str {
30        " {}"
31    }
32
33    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
34        extract_docstring(node, content)
35    }
36
37    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
38        extract_attributes(node, content)
39    }
40
41    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
42        if node.kind() == "impl_item" {
43            let type_node = match node.child_by_field_name("type") {
44                Some(n) => n,
45                None => return crate::ImplementsInfo::default(),
46            };
47            let _ = &content[type_node.byte_range()]; // used below
48            let is_interface = node.child_by_field_name("trait").is_some();
49            let implements = if let Some(trait_node) = node.child_by_field_name("trait") {
50                vec![content[trait_node.byte_range()].to_string()]
51            } else {
52                Vec::new()
53            };
54            crate::ImplementsInfo {
55                is_interface,
56                implements,
57            }
58        } else {
59            crate::ImplementsInfo::default()
60        }
61    }
62
63    fn refine_kind(
64        &self,
65        node: &Node,
66        _content: &str,
67        tag_kind: crate::SymbolKind,
68    ) -> crate::SymbolKind {
69        match node.kind() {
70            "struct_item" => crate::SymbolKind::Struct,
71            "enum_item" => crate::SymbolKind::Enum,
72            "type_item" => crate::SymbolKind::Type,
73            "union_item" => crate::SymbolKind::Struct,
74            "trait_item" => crate::SymbolKind::Trait,
75            _ => tag_kind,
76        }
77    }
78
79    fn build_signature(&self, node: &Node, content: &str) -> String {
80        match node.kind() {
81            "function_item" | "function_signature_item" => {
82                let name = match self.node_name(node, content) {
83                    Some(n) => n,
84                    None => {
85                        return content[node.byte_range()]
86                            .lines()
87                            .next()
88                            .unwrap_or("")
89                            .trim()
90                            .to_string();
91                    }
92                };
93                let vis = self.extract_visibility_prefix(node, content);
94                let params = node
95                    .child_by_field_name("parameters")
96                    .map(|p| content[p.byte_range()].to_string())
97                    .unwrap_or_else(|| "()".to_string());
98                let return_type = node
99                    .child_by_field_name("return_type")
100                    .map(|r| format!(" -> {}", &content[r.byte_range()]))
101                    .unwrap_or_default();
102                format!("{}fn {}{}{}", vis, name, params, return_type)
103            }
104            "impl_item" => {
105                let type_node = node.child_by_field_name("type");
106                let type_name = type_node
107                    .map(|n| content[n.byte_range()].to_string())
108                    .unwrap_or_default();
109                if let Some(trait_node) = node.child_by_field_name("trait") {
110                    let trait_name = &content[trait_node.byte_range()];
111                    format!("impl {} for {}", trait_name, type_name)
112                } else {
113                    format!("impl {}", type_name)
114                }
115            }
116            "trait_item" => {
117                let name = self.node_name(node, content).unwrap_or("");
118                let vis = self.extract_visibility_prefix(node, content);
119                format!("{}trait {}", vis, name)
120            }
121            "mod_item" => {
122                let name = self.node_name(node, content).unwrap_or("");
123                let vis = self.extract_visibility_prefix(node, content);
124                format!("{}mod {}", vis, name)
125            }
126            "struct_item" => {
127                let name = self.node_name(node, content).unwrap_or("");
128                let vis = self.extract_visibility_prefix(node, content);
129                format!("{}struct {}", vis, name)
130            }
131            "enum_item" => {
132                let name = self.node_name(node, content).unwrap_or("");
133                let vis = self.extract_visibility_prefix(node, content);
134                format!("{}enum {}", vis, name)
135            }
136            "type_item" => {
137                let name = self.node_name(node, content).unwrap_or("");
138                let vis = self.extract_visibility_prefix(node, content);
139                format!("{}type {}", vis, name)
140            }
141            _ => {
142                let text = &content[node.byte_range()];
143                text.lines().next().unwrap_or(text).trim().to_string()
144            }
145        }
146    }
147
148    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
149        if node.kind() != "use_declaration" {
150            return Vec::new();
151        }
152
153        let line = node.start_position().row + 1;
154        let text = &content[node.byte_range()];
155        let module = text.trim_start_matches("use ").trim_end_matches(';').trim();
156
157        // Check for braced imports: use foo::{bar, baz}
158        let mut names = Vec::new();
159        let is_relative = module.starts_with("crate")
160            || module.starts_with("self")
161            || module.starts_with("super");
162
163        if let Some(brace_start) = module.find('{') {
164            let prefix = module[..brace_start].trim_end_matches("::");
165            // Find matching closing brace using depth counter to handle nested groups
166            // like `use std::{io::{Read, Write}, fs}`.
167            let brace_end = {
168                let mut depth = 0u32;
169                let mut end = None;
170                for (i, c) in module[brace_start..].char_indices() {
171                    match c {
172                        '{' => depth += 1,
173                        '}' => {
174                            depth -= 1;
175                            if depth == 0 {
176                                end = Some(brace_start + i);
177                                break;
178                            }
179                        }
180                        _ => {}
181                    }
182                }
183                end
184            };
185            if let Some(brace_end) = brace_end {
186                let items = &module[brace_start + 1..brace_end];
187                for item in items.split(',') {
188                    let trimmed = item.trim();
189                    if !trimmed.is_empty() {
190                        names.push(trimmed.to_string());
191                    }
192                }
193            }
194            vec![Import {
195                module: prefix.to_string(),
196                names,
197                alias: None,
198                is_wildcard: false,
199                is_relative,
200                line,
201            }]
202        } else {
203            // Simple import: use foo::bar or use foo::bar as baz
204            let (module_part, alias) = if let Some(as_pos) = module.find(" as ") {
205                (&module[..as_pos], Some(module[as_pos + 4..].to_string()))
206            } else {
207                (module, None)
208            };
209
210            vec![Import {
211                module: module_part.to_string(),
212                names: Vec::new(),
213                alias,
214                is_wildcard: module_part.ends_with("::*"),
215                is_relative,
216                line,
217            }]
218        }
219    }
220
221    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
222        let names_to_use: Vec<&str> = names
223            .map(|n| n.to_vec())
224            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
225
226        if import.is_wildcard {
227            // Module already contains ::* from parsing
228            format!("use {};", import.module)
229        } else if names_to_use.is_empty() {
230            format!("use {};", import.module)
231        } else if names_to_use.len() == 1 {
232            format!("use {}::{};", import.module, names_to_use[0])
233        } else {
234            format!("use {}::{{{}}};", import.module, names_to_use.join(", "))
235        }
236    }
237
238    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
239        let mut cursor = node.walk();
240        for child in node.children(&mut cursor) {
241            if child.kind() == "visibility_modifier" {
242                let vis = &content[child.byte_range()];
243                if vis == "pub" {
244                    return Visibility::Public;
245                } else if vis.starts_with("pub(crate)") {
246                    return Visibility::Internal;
247                } else if vis.starts_with("pub(super)") || vis.starts_with("pub(in") {
248                    return Visibility::Protected;
249                }
250            }
251        }
252        Visibility::Private
253    }
254
255    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
256        let in_attrs = symbol
257            .attributes
258            .iter()
259            .any(|a| a.contains("#[test]") || a.contains("#[cfg(test)]"));
260        let in_sig =
261            symbol.signature.contains("#[test]") || symbol.signature.contains("#[cfg(test)]");
262        if in_attrs || in_sig {
263            return true;
264        }
265        match symbol.kind {
266            crate::SymbolKind::Function | crate::SymbolKind::Method => {
267                symbol.name.starts_with("test_")
268            }
269            crate::SymbolKind::Module => symbol.name == "tests",
270            _ => false,
271        }
272    }
273
274    fn test_file_globs(&self) -> &'static [&'static str] {
275        &[
276            "**/tests/**",
277            "**/test_*.rs",
278            "**/*_test.rs",
279            "**/*_tests.rs",
280        ]
281    }
282
283    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
284        node.child_by_field_name("body")
285    }
286
287    fn analyze_container_body(
288        &self,
289        body_node: &Node,
290        content: &str,
291        inner_indent: &str,
292    ) -> Option<ContainerBody> {
293        crate::body::analyze_brace_body(body_node, content, inner_indent)
294    }
295
296    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
297        // impl_item uses "type" field; trait_item and mod_item use "name"
298        let name_node = node
299            .child_by_field_name("name")
300            .or_else(|| node.child_by_field_name("type"))?;
301        Some(&content[name_node.byte_range()])
302    }
303
304    fn extract_module_doc(&self, src: &str) -> Option<String> {
305        extract_rust_module_doc(src)
306    }
307
308    fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
309        static RESOLVER: RustModuleResolver = RustModuleResolver;
310        Some(&RESOLVER)
311    }
312}
313
314impl LanguageSymbols for Rust {}
315
316/// Module resolver for Rust (Cargo workspace).
317pub struct RustModuleResolver;
318
319impl ModuleResolver for RustModuleResolver {
320    fn workspace_config(&self, root: &Path) -> ResolverConfig {
321        let cargo_toml = root.join("Cargo.toml");
322        let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
323
324        if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
325            // Try workspace members
326            if let Ok(val) = content.parse::<toml::Value>() {
327                if let Some(members) = val
328                    .get("workspace")
329                    .and_then(|w| w.get("members"))
330                    .and_then(|m| m.as_array())
331                {
332                    for member in members {
333                        if let Some(member_str) = member.as_str() {
334                            // Expand glob patterns (simple case: no actual glob, just list)
335                            let member_path = root.join(member_str);
336                            let member_cargo = member_path.join("Cargo.toml");
337                            if let Ok(mc) = std::fs::read_to_string(&member_cargo)
338                                && let Ok(mv) = mc.parse::<toml::Value>()
339                                && let Some(name) = mv
340                                    .get("package")
341                                    .and_then(|p| p.get("name"))
342                                    .and_then(|n| n.as_str())
343                            {
344                                path_mappings.push((name.to_string(), member_path));
345                            }
346                        }
347                    }
348                }
349
350                // Single-crate: read package name from root Cargo.toml
351                if path_mappings.is_empty()
352                    && let Some(name) = val
353                        .get("package")
354                        .and_then(|p| p.get("name"))
355                        .and_then(|n| n.as_str())
356                {
357                    path_mappings.push((name.to_string(), root.to_path_buf()));
358                }
359            }
360        }
361
362        ResolverConfig {
363            workspace_root: root.to_path_buf(),
364            path_mappings,
365            search_roots: Vec::new(),
366        }
367    }
368
369    fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
370        // Find the crate this file belongs to
371        for (crate_name, crate_dir) in &cfg.path_mappings {
372            let src_dir = crate_dir.join("src");
373            if let Ok(rel) = file.strip_prefix(&src_dir) {
374                let components: Vec<&str> = rel
375                    .components()
376                    .filter_map(|c| {
377                        if let std::path::Component::Normal(s) = c {
378                            s.to_str()
379                        } else {
380                            None
381                        }
382                    })
383                    .collect();
384
385                if components.is_empty() {
386                    continue;
387                }
388
389                let last = *components.last().unwrap();
390                let module_path =
391                    if components.len() == 1 && (last == "lib.rs" || last == "main.rs") {
392                        // Crate root
393                        crate_name.clone()
394                    } else {
395                        // Build module path from components
396                        let mut parts = vec![crate_name.as_str()];
397                        for c in &components[..components.len() - 1] {
398                            parts.push(c);
399                        }
400                        // Last component: strip .rs, handle mod.rs
401                        let stem = if last == "mod.rs" {
402                            // mod.rs is the module named by its parent directory
403                            // (already included in parts above)
404                            None
405                        } else {
406                            last.strip_suffix(".rs")
407                        };
408                        if let Some(s) = stem {
409                            parts.push(s);
410                        }
411                        parts.join("::")
412                    };
413
414                return vec![ModuleId {
415                    canonical_path: module_path,
416                }];
417            }
418        }
419        Vec::new()
420    }
421
422    fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
423        // Only handle .rs files
424        if from_file.extension().and_then(|e| e.to_str()) != Some("rs") {
425            return Resolution::NotApplicable;
426        }
427
428        let raw = &spec.raw;
429
430        // Handle super:: and self:: relative paths
431        let resolved_raw = if raw.starts_with("super::") || raw.starts_with("self::") {
432            // Resolve relative to from_file's module
433            let crate_name = self.crate_name_for_file(from_file, cfg);
434            if let Some(cn) = crate_name {
435                let module_path = self.module_path_for_file(from_file, cfg);
436                if let Some(suffix) = raw.strip_prefix("super::") {
437                    // Pop one level from module path
438                    let parent = module_path
439                        .rsplit_once("::")
440                        .map(|(p, _)| p.to_string())
441                        .unwrap_or_else(|| cn.clone());
442                    format!("{}::{}", parent, suffix)
443                } else {
444                    let suffix = raw.strip_prefix("self::").unwrap_or(raw);
445                    format!("{}::{}", module_path, suffix)
446                }
447            } else {
448                return Resolution::NotFound;
449            }
450        } else {
451            raw.clone()
452        };
453
454        // Try to find the crate for this path
455        let (crate_name, rest) = if let Some(pos) = resolved_raw.find("::") {
456            let cn = &resolved_raw[..pos];
457            let rest = &resolved_raw[pos + 2..];
458            (cn.to_string(), rest.to_string())
459        } else {
460            (resolved_raw.clone(), String::new())
461        };
462
463        // Look up crate in workspace
464        let crate_dir = cfg
465            .path_mappings
466            .iter()
467            .find(|(name, _)| name == &crate_name)
468            .map(|(_, dir)| dir.clone());
469
470        let crate_dir = match crate_dir {
471            Some(d) => d,
472            None => {
473                // Could be stdlib or external — not resolvable
474                return Resolution::NotFound;
475            }
476        };
477
478        // Convert module path to file path
479        let exported_name = if let Some(pos) = rest.rfind("::") {
480            rest[pos + 2..].to_string()
481        } else {
482            rest.clone()
483        };
484
485        let module_part = if let Some(pos) = rest.rfind("::") {
486            rest[..pos].to_string()
487        } else {
488            String::new()
489        };
490
491        let candidate = self.module_to_file(&crate_dir, &module_part);
492        if let Some(path) = candidate {
493            Resolution::Resolved(path, exported_name)
494        } else {
495            Resolution::NotFound
496        }
497    }
498}
499
500impl RustModuleResolver {
501    /// Get the crate name for a file.
502    fn crate_name_for_file(&self, file: &Path, cfg: &ResolverConfig) -> Option<String> {
503        for (crate_name, crate_dir) in &cfg.path_mappings {
504            if file.starts_with(crate_dir) {
505                return Some(crate_name.clone());
506            }
507        }
508        None
509    }
510
511    /// Get the module path string for a file.
512    fn module_path_for_file(&self, file: &Path, cfg: &ResolverConfig) -> String {
513        for (crate_name, crate_dir) in &cfg.path_mappings {
514            let src_dir = crate_dir.join("src");
515            if let Ok(rel) = file.strip_prefix(&src_dir) {
516                let components: Vec<&str> = rel
517                    .components()
518                    .filter_map(|c| {
519                        if let std::path::Component::Normal(s) = c {
520                            s.to_str()
521                        } else {
522                            None
523                        }
524                    })
525                    .collect();
526                if components.is_empty() {
527                    return crate_name.clone();
528                }
529                let last = *components.last().unwrap();
530                if components.len() == 1 && (last == "lib.rs" || last == "main.rs") {
531                    return crate_name.clone();
532                }
533                let mut parts = vec![crate_name.as_str()];
534                for c in &components[..components.len() - 1] {
535                    parts.push(c);
536                }
537                if last != "mod.rs"
538                    && let Some(s) = last.strip_suffix(".rs")
539                {
540                    parts.push(s);
541                }
542                return parts.join("::");
543            }
544        }
545        String::new()
546    }
547
548    /// Convert a module path to a file path within a crate directory.
549    ///
550    /// e.g. "foo::bar" → tries `src/foo/bar.rs` and `src/foo/bar/mod.rs`
551    fn module_to_file(&self, crate_dir: &Path, module_path: &str) -> Option<PathBuf> {
552        let src_dir = crate_dir.join("src");
553
554        if module_path.is_empty() {
555            // Refers to the crate root
556            let lib = src_dir.join("lib.rs");
557            if lib.exists() {
558                return Some(lib);
559            }
560            let main = src_dir.join("main.rs");
561            if main.exists() {
562                return Some(main);
563            }
564            return None;
565        }
566
567        let parts: Vec<&str> = module_path.split("::").collect();
568        let mut path = src_dir.clone();
569        for part in &parts {
570            path = path.join(part);
571        }
572
573        // Try path.rs first
574        let rs_path = path.with_extension("rs");
575        if rs_path.exists() {
576            return Some(rs_path);
577        }
578
579        // Try path/mod.rs
580        let mod_path = path.join("mod.rs");
581        if mod_path.exists() {
582            return Some(mod_path);
583        }
584
585        None
586    }
587}
588
589impl Rust {
590    fn extract_visibility_prefix(&self, node: &Node, content: &str) -> String {
591        let mut cursor = node.walk();
592        for child in node.children(&mut cursor) {
593            if child.kind() == "visibility_modifier" {
594                return format!("{} ", &content[child.byte_range()]);
595            }
596        }
597        String::new()
598    }
599}
600
601/// Extract a Rust doc comment from a node's `attributes` child.
602///
603/// Looks for `line_outer_doc_comment` nodes (`///`) and joins their text.
604fn extract_docstring(node: &Node, content: &str) -> Option<String> {
605    let mut cursor = node.walk();
606    for child in node.children(&mut cursor) {
607        if child.kind() == "attributes" {
608            let mut doc_lines = Vec::new();
609            let mut attr_cursor = child.walk();
610            for attr_child in child.children(&mut attr_cursor) {
611                if attr_child.kind() == "line_outer_doc_comment" {
612                    let text = &content[attr_child.byte_range()];
613                    let doc = text.trim_start_matches("///").trim();
614                    if !doc.is_empty() {
615                        doc_lines.push(doc.to_string());
616                    }
617                }
618            }
619            if !doc_lines.is_empty() {
620                return Some(doc_lines.join("\n"));
621            }
622        }
623    }
624    None
625}
626
627/// Extract Rust `#[...]` attribute items from a node.
628///
629/// Checks both the `attributes` child field and preceding sibling `attribute_item` nodes.
630fn extract_attributes(node: &Node, content: &str) -> Vec<String> {
631    let mut attrs = Vec::new();
632
633    // Check for attributes child (e.g., #[test], #[cfg(test)])
634    if let Some(attr_node) = node.child_by_field_name("attributes") {
635        let mut cursor = attr_node.walk();
636        for child in attr_node.children(&mut cursor) {
637            if child.kind() == "attribute_item" {
638                attrs.push(content[child.byte_range()].to_string());
639            }
640        }
641    }
642
643    // Also check preceding siblings for outer attributes
644    let mut prev = node.prev_sibling();
645    while let Some(sibling) = prev {
646        if sibling.kind() == "attribute_item" {
647            // Insert at beginning to maintain order
648            attrs.insert(0, content[sibling.byte_range()].to_string());
649            prev = sibling.prev_sibling();
650        } else {
651            break;
652        }
653    }
654
655    attrs
656}
657
658/// Extract the module-level doc comment from Rust source.
659///
660/// Collects consecutive `//!` inner-doc comment lines from the top of the file,
661/// stopping at the first line that is not a `//!` comment (ignoring blank lines).
662fn extract_rust_module_doc(src: &str) -> Option<String> {
663    let mut lines = Vec::new();
664    for line in src.lines() {
665        let trimmed = line.trim();
666        if trimmed.starts_with("//!") {
667            let text = trimmed.strip_prefix("//!").unwrap_or("").trim_start();
668            lines.push(text.to_string());
669        } else if trimmed.is_empty() && lines.is_empty() {
670            // skip leading blank lines
671        } else {
672            break;
673        }
674    }
675    if lines.is_empty() {
676        return None;
677    }
678    // Strip trailing empty lines from the collected doc
679    while lines.last().map(|l: &String| l.is_empty()).unwrap_or(false) {
680        lines.pop();
681    }
682    if lines.is_empty() {
683        None
684    } else {
685        Some(lines.join("\n"))
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use crate::validate_unused_kinds_audit;
693
694    /// Documents node kinds that exist in the Rust grammar but aren't used in trait methods.
695    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
696    #[test]
697    fn unused_node_kinds_audit() {
698        // Categories:
699        // - STRUCTURAL: Internal/wrapper nodes
700        // - CLAUSE: Sub-parts of larger constructs
701        // - EXPRESSION: Expressions (we track statements/definitions)
702        // - TYPE: Type-related nodes
703        // - MODIFIER: Visibility/async/unsafe modifiers
704        // - PATTERN: Pattern matching internals
705        // - MACRO: Macro-related nodes
706        // - MAYBE: Potentially useful
707
708        #[rustfmt::skip]
709        let documented_unused: &[&str] = &[
710            // STRUCTURAL
711            "block_comment",           // comments        // extern block contents
712            "field_declaration",       // struct field
713            "field_declaration_list",  // struct body
714            "field_expression",        // foo.bar
715            "field_identifier",        // field name
716            "identifier",              // too common
717            "lifetime",                // 'a
718            "lifetime_parameter",      // <'a>
719            "ordered_field_declaration_list", // tuple struct fields
720            "scoped_identifier",       // path::to::thing
721            "scoped_type_identifier",  // path::to::Type
722            "shorthand_field_identifier", // struct init shorthand
723            "type_identifier",         // type names
724            "visibility_modifier",     // pub, pub(crate)
725
726            // CLAUSE
727            "else_clause",             // part of if
728            "enum_variant",            // enum variant
729            "enum_variant_list",       // enum body
730            "match_block",             // match body
731            "match_pattern",           // match arm pattern
732            "trait_bounds",            // T: Foo + Bar
733            "where_clause",            // where T: Foo
734
735            // EXPRESSION
736            "array_expression",        // [1, 2, 3]
737            "assignment_expression",   // x = y
738            "async_block",             // async { }
739            "await_expression",        // foo.await         // foo()
740            "generic_function",        // foo::<T>()
741            "index_expression",        // arr[i]
742            "parenthesized_expression",// (expr)
743            "range_expression",        // 0..10
744            "reference_expression",    // &x
745            "struct_expression",       // Foo { x: 1 }
746            "try_expression",          // foo?
747            "tuple_expression",        // (a, b)
748            "type_cast_expression",    // x as T
749            "unary_expression",        // -x, !x
750            "unit_expression",         // ()
751            "yield_expression",        // yield x
752
753            // TYPE
754            "abstract_type",           // impl Trait
755            "array_type",              // [T; N]
756            "bounded_type",            // T: Foo
757            "bracketed_type",          // <T>
758            "dynamic_type",            // dyn Trait
759            "function_type",           // fn(T) -> U
760            "generic_type",            // Vec<T>
761            "generic_type_with_turbofish", // Vec::<T>
762            "higher_ranked_trait_bound", // for<'a>
763            "never_type",              // !
764            "pointer_type",            // *const T
765            "primitive_type",          // i32, bool
766            "qualified_type",          // <T as Trait>::Item
767            "reference_type",          // &T
768            "removed_trait_bound",     // ?Sized
769            "tuple_type",              // (A, B)
770            "type_arguments",          // <T, U>
771            "type_binding",            // Item = T
772            "type_parameter",          // T
773            "type_parameters",         // <T, U>
774            "unit_type",               // ()
775            "unsafe_bound_type",       // unsafe trait bound
776
777            // MODIFIER
778            "block_outer_doc_comment", // //!
779            "extern_modifier",         // extern "C"
780            "function_modifiers",      // async, const, unsafe
781            "mutable_specifier",       // mut
782
783            // PATTERN
784            "struct_pattern",          // Foo { x, y }
785            "tuple_struct_pattern",    // Foo(x, y)
786
787            // MACRO
788            "fragment_specifier",      // $x:expr
789            "macro_arguments_declaration", // macro args
790            "macro_body_v2",           // macro body        // macro_rules!
791            "macro_definition_v2",     // macro 2.0
792
793            // OTHER
794            "block_expression_with_attribute", // #[attr] { }
795            "const_block",             // const { }
796            "expression_statement",    // expr;
797            "expression_with_attribute", // #[attr] expr
798            "extern_crate_declaration",// extern crate
799            "foreign_mod_item",        // extern block item
800            "function_signature_item", // fn signature in trait
801            "gen_block",               // gen { }
802            "let_declaration",         // let x = y
803            "try_block",               // try { }
804            "unsafe_block",            // unsafe { }
805            "use_as_clause",           // use foo as bar
806            "empty_statement",         // ;
807            // control flow — not extracted as symbols
808            "closure_expression",
809            "continue_expression",
810            "match_expression",
811            "use_declaration",
812            "for_expression",
813            "match_arm",
814            "break_expression",
815            "while_expression",
816            "loop_expression",
817            "return_expression",
818            "if_expression",
819            "block",
820            "binary_expression",
821        ];
822
823        validate_unused_kinds_audit(&Rust, documented_unused)
824            .expect("Rust unused node kinds audit failed");
825    }
826}