Skip to main content

normalize_languages/
typescript.rs

1//! TypeScript language support.
2
3use crate::ecmascript;
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7/// TypeScript language support.
8pub struct TypeScript;
9
10/// TSX language support (TypeScript + JSX).
11pub struct Tsx;
12
13impl Language for TypeScript {
14    fn name(&self) -> &'static str {
15        "TypeScript"
16    }
17    fn extensions(&self) -> &'static [&'static str] {
18        &["ts", "mts", "cts"]
19    }
20    fn grammar_name(&self) -> &'static str {
21        "typescript"
22    }
23
24    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
25        Some(self)
26    }
27
28    fn signature_suffix(&self) -> &'static str {
29        " {}"
30    }
31
32    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
33        ecmascript::extract_jsdoc(node, content)
34    }
35
36    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
37        ecmascript::extract_implements(node, content)
38    }
39
40    fn build_signature(&self, node: &Node, content: &str) -> String {
41        let name = match self.node_name(node, content) {
42            Some(n) => n,
43            None => {
44                return content[node.byte_range()]
45                    .lines()
46                    .next()
47                    .unwrap_or("")
48                    .trim()
49                    .to_string();
50            }
51        };
52        ecmascript::build_signature(node, content, name)
53    }
54
55    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
56        ecmascript::extract_imports(node, content)
57    }
58
59    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
60        ecmascript::format_import(import, names)
61    }
62
63    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
64        let name = symbol.name.as_str();
65        match symbol.kind {
66            crate::SymbolKind::Function | crate::SymbolKind::Method => {
67                name.starts_with("test_")
68                    || name.starts_with("Test")
69                    || name == "describe"
70                    || name == "it"
71                    || name == "test"
72            }
73            crate::SymbolKind::Module => name == "tests" || name == "test" || name == "__tests__",
74            _ => false,
75        }
76    }
77
78    fn test_file_globs(&self) -> &'static [&'static str] {
79        &[
80            "**/__tests__/**/*.ts",
81            "**/__mocks__/**/*.ts",
82            "**/*.test.ts",
83            "**/*.spec.ts",
84            "**/*.test.tsx",
85            "**/*.spec.tsx",
86        ]
87    }
88
89    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
90        ecmascript::extract_decorators(node, content)
91    }
92
93    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
94        // Try 'body' field first, then look for interface_body or class_body child
95        if let Some(body) = node.child_by_field_name("body") {
96            return Some(body);
97        }
98        // Fallback: find interface_body or class_body child
99        for i in 0..node.child_count() as u32 {
100            if let Some(child) = node.child(i)
101                && (child.kind() == "interface_body" || child.kind() == "class_body")
102            {
103                return Some(child);
104            }
105        }
106        None
107    }
108
109    fn analyze_container_body(
110        &self,
111        body_node: &Node,
112        content: &str,
113        inner_indent: &str,
114    ) -> Option<ContainerBody> {
115        crate::body::analyze_brace_body(body_node, content, inner_indent)
116    }
117
118    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
119        ecmascript::get_visibility(node, content)
120    }
121
122    fn extract_module_doc(&self, src: &str) -> Option<String> {
123        ecmascript::extract_js_module_doc(src)
124    }
125}
126
127impl LanguageSymbols for TypeScript {}
128
129// TSX shares the same implementation as TypeScript, just with a different grammar
130impl Language for Tsx {
131    fn name(&self) -> &'static str {
132        "TSX"
133    }
134    fn extensions(&self) -> &'static [&'static str] {
135        &["tsx"]
136    }
137    fn grammar_name(&self) -> &'static str {
138        "tsx"
139    }
140
141    fn signature_suffix(&self) -> &'static str {
142        " {}"
143    }
144
145    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
146        ecmascript::extract_jsdoc(node, content)
147    }
148
149    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
150        ecmascript::extract_implements(node, content)
151    }
152
153    fn build_signature(&self, node: &Node, content: &str) -> String {
154        let name = match self.node_name(node, content) {
155            Some(n) => n,
156            None => {
157                return content[node.byte_range()]
158                    .lines()
159                    .next()
160                    .unwrap_or("")
161                    .trim()
162                    .to_string();
163            }
164        };
165        ecmascript::build_signature(node, content, name)
166    }
167
168    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
169        ecmascript::extract_imports(node, content)
170    }
171
172    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
173        ecmascript::format_import(import, names)
174    }
175
176    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
177        let name = symbol.name.as_str();
178        match symbol.kind {
179            crate::SymbolKind::Function | crate::SymbolKind::Method => {
180                name.starts_with("test_")
181                    || name.starts_with("Test")
182                    || name == "describe"
183                    || name == "it"
184                    || name == "test"
185            }
186            crate::SymbolKind::Module => name == "tests" || name == "test" || name == "__tests__",
187            _ => false,
188        }
189    }
190
191    fn test_file_globs(&self) -> &'static [&'static str] {
192        &[
193            "**/__tests__/**/*.ts",
194            "**/__mocks__/**/*.ts",
195            "**/*.test.ts",
196            "**/*.spec.ts",
197            "**/*.test.tsx",
198            "**/*.spec.tsx",
199        ]
200    }
201
202    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
203        ecmascript::extract_decorators(node, content)
204    }
205
206    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
207        // Try 'body' field first, then look for interface_body or class_body child
208        if let Some(body) = node.child_by_field_name("body") {
209            return Some(body);
210        }
211        // Fallback: find interface_body or class_body child
212        for i in 0..node.child_count() as u32 {
213            if let Some(child) = node.child(i)
214                && (child.kind() == "interface_body" || child.kind() == "class_body")
215            {
216                return Some(child);
217            }
218        }
219        None
220    }
221
222    fn analyze_container_body(
223        &self,
224        body_node: &Node,
225        content: &str,
226        inner_indent: &str,
227    ) -> Option<ContainerBody> {
228        crate::body::analyze_brace_body(body_node, content, inner_indent)
229    }
230
231    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
232        ecmascript::get_visibility(node, content)
233    }
234
235    fn extract_module_doc(&self, src: &str) -> Option<String> {
236        ecmascript::extract_js_module_doc(src)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::validate_unused_kinds_audit;
244
245    /// Documents node kinds that exist in the TypeScript grammar but aren't used in trait methods.
246    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
247    #[test]
248    fn unused_node_kinds_audit() {
249        #[rustfmt::skip]
250        let documented_unused: &[&str] = &[
251            // STRUCTURAL
252            "class_body",              // class body block
253            "class_heritage",          // extends clause
254            "class_static_block",      // static { }
255            "enum_assignment",         // enum value assignment
256            "enum_body",               // enum body
257            "formal_parameters",       // function params
258            "identifier",              // too common
259            "interface_body",          // interface body
260            "nested_identifier",       // a.b.c path
261            "nested_type_identifier",  // a.b.Type path
262            "private_property_identifier", // #field
263            "property_identifier",     // obj.prop
264            "public_field_definition", // class field
265            "shorthand_property_identifier", // { x } shorthand
266            "shorthand_property_identifier_pattern", // destructuring
267            "statement_block",         // { }
268            "statement_identifier",    // label name
269            "switch_body",             // switch cases
270
271            // CLAUSE
272            "default_type",            // default type param
273            "else_clause",             // else branch
274            "extends_clause",          // class extends
275            "extends_type_clause",     // T extends U
276            "finally_clause",          // finally block
277            "implements_clause",       // implements X
278
279            // EXPRESSION
280            "as_expression",           // x as T
281            "assignment_expression",   // x = y
282            "augmented_assignment_expression", // x += y
283            "await_expression",        // await foo
284            "call_expression",         // foo()
285            "function_expression",     // function() {}
286            "instantiation_expression",// generic call
287            "member_expression",       // foo.bar          // new Foo()
288            "non_null_expression",     // x!
289            "parenthesized_expression",// (expr)
290            "satisfies_expression",    // x satisfies T
291            "sequence_expression",     // a, b
292            "subscript_expression",    // arr[i]
293            "unary_expression",        // -x, !x
294            "update_expression",       // x++
295            "yield_expression",        // yield x
296
297            // TYPE NODES
298            "adding_type_annotation",  // : T
299            "array_type",              // T[]
300            "conditional_type",        // T extends U ? V : W
301            "construct_signature",     // new(): T
302            "constructor_type",        // new (x: T) => U
303            "existential_type",        // *
304            "flow_maybe_type",         // ?T      // function sig
305            "function_type",           // (x: T) => U
306            "generic_type",            // T<U>
307            "index_type_query",        // keyof T
308            "infer_type",              // infer T
309            "intersection_type",       // T & U
310            "literal_type",            // "foo" type
311            "lookup_type",             // T[K]
312            "mapped_type_clause",      // [K in T]
313            "object_type",             // { x: T }
314            "omitting_type_annotation",// omit annotation
315            "opting_type_annotation",  // optional annotation
316            "optional_type",           // T?
317            "override_modifier",       // override
318            "parenthesized_type",      // (T)
319            "predefined_type",         // string, number
320            "readonly_type",           // readonly T
321            "rest_type",               // ...T
322            "template_literal_type",   // `${T}`
323            "template_type",           // template type
324            "this_type",               // this
325            "tuple_type",              // [T, U]         // : T
326            "type_arguments",          // <T, U>
327            "type_assertion",          // <T>x         // type name
328            "type_parameter",          // T
329            "type_parameters",         // <T, U>
330            "type_predicate",          // x is T
331            "type_predicate_annotation", // : x is T
332            "type_query",              // typeof x
333            "union_type",              // T | U
334
335            // IMPORT/EXPORT DETAILS
336            "accessibility_modifier",  // public/private/protected
337            "export_clause",           // export { a, b }
338            "export_specifier",        // export { a as b }
339            "import",                  // import keyword
340            "import_alias",            // import X = Y
341            "import_attribute",        // import attributes
342            "import_clause",           // import clause
343            "import_require_clause",   // require()
344            "import_specifier",        // import { a }
345            "named_imports",           // { a, b }
346            "namespace_export",        // export * as ns
347            "namespace_import",        // import * as ns
348
349            // DECLARATION // abstract class // abstract method
350            "ambient_declaration",     // declare
351            "debugger_statement",      // debugger;
352            "empty_statement",         // ;
353            "expression_statement",    // expr;
354            "generator_function",      // function* foo
355            "generator_function_declaration", // function* declaration
356            "internal_module",         // namespace/module
357            "labeled_statement",       // label: stmt
358            "lexical_declaration",     // let/const                  // module keyword
359            "using_declaration",       // using x = ...
360            "variable_declaration",    // var x
361            "with_statement",          // with (obj) - deprecated
362            // control flow — not extracted as symbols
363            "for_in_statement",
364            "switch_case",
365            "continue_statement",
366            "do_statement",
367            "return_statement",
368            "class",
369            "switch_statement",
370            "binary_expression",
371            "while_statement",
372            "for_statement",
373            "if_statement",
374            "throw_statement",
375            "try_statement",
376            "break_statement",
377            "arrow_function",
378            "catch_clause",
379            "ternary_expression",
380            "import_statement",
381            "export_statement",
382        ];
383
384        validate_unused_kinds_audit(&TypeScript, documented_unused)
385            .expect("TypeScript unused node kinds audit failed");
386    }
387}