Skip to main content

normalize_languages/
typescript.rs

1//! TypeScript language support.
2
3use crate::ecmascript;
4use crate::external_packages::ResolvedPackage;
5use crate::{Export, Import, Language, Symbol, Visibility, VisibilityMechanism};
6use std::path::{Path, PathBuf};
7use tree_sitter::Node;
8
9/// TypeScript language support.
10pub struct TypeScript;
11
12/// TSX language support (TypeScript + JSX).
13pub struct Tsx;
14
15impl Language for TypeScript {
16    fn name(&self) -> &'static str {
17        "TypeScript"
18    }
19    fn extensions(&self) -> &'static [&'static str] {
20        &["ts", "mts", "cts"]
21    }
22    fn grammar_name(&self) -> &'static str {
23        "typescript"
24    }
25
26    fn has_symbols(&self) -> bool {
27        true
28    }
29
30    fn container_kinds(&self) -> &'static [&'static str] {
31        ecmascript::TS_CONTAINER_KINDS
32    }
33    fn function_kinds(&self) -> &'static [&'static str] {
34        ecmascript::TS_FUNCTION_KINDS
35    }
36    fn type_kinds(&self) -> &'static [&'static str] {
37        ecmascript::TS_TYPE_KINDS
38    }
39    fn import_kinds(&self) -> &'static [&'static str] {
40        ecmascript::IMPORT_KINDS
41    }
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        ecmascript::PUBLIC_SYMBOL_KINDS
44    }
45    fn visibility_mechanism(&self) -> VisibilityMechanism {
46        VisibilityMechanism::ExplicitExport
47    }
48    fn scope_creating_kinds(&self) -> &'static [&'static str] {
49        ecmascript::SCOPE_CREATING_KINDS
50    }
51    fn control_flow_kinds(&self) -> &'static [&'static str] {
52        ecmascript::CONTROL_FLOW_KINDS
53    }
54    fn complexity_nodes(&self) -> &'static [&'static str] {
55        ecmascript::COMPLEXITY_NODES
56    }
57    fn nesting_nodes(&self) -> &'static [&'static str] {
58        ecmascript::NESTING_NODES
59    }
60
61    fn signature_suffix(&self) -> &'static str {
62        " {}"
63    }
64
65    fn extract_function(&self, node: &Node, content: &str, in_container: bool) -> Option<Symbol> {
66        let name = self.node_name(node, content)?;
67        Some(ecmascript::extract_function(
68            node,
69            content,
70            in_container,
71            name,
72        ))
73    }
74
75    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
76        let name = self.node_name(node, content)?;
77        Some(ecmascript::extract_container(node, content, name))
78    }
79
80    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
81        let name = self.node_name(node, content)?;
82        ecmascript::extract_type(node, name)
83    }
84
85    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
86        ecmascript::extract_imports(node, content)
87    }
88
89    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
90        ecmascript::format_import(import, names)
91    }
92
93    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
94        ecmascript::extract_public_symbols(node, content)
95    }
96
97    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
98        None
99    }
100
101    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
102        Vec::new()
103    }
104
105    fn is_public(&self, _node: &Node, _content: &str) -> bool {
106        true
107    }
108
109    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
110        Visibility::Public
111    }
112
113    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
114        let name = symbol.name.as_str();
115        match symbol.kind {
116            crate::SymbolKind::Function | crate::SymbolKind::Method => {
117                name.starts_with("test_")
118                    || name.starts_with("Test")
119                    || name == "describe"
120                    || name == "it"
121                    || name == "test"
122            }
123            crate::SymbolKind::Module => name == "tests" || name == "test" || name == "__tests__",
124            _ => false,
125        }
126    }
127
128    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
129        None
130    }
131
132    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
133        // Try 'body' field first, then look for interface_body or class_body child
134        if let Some(body) = node.child_by_field_name("body") {
135            return Some(body);
136        }
137        // Fallback: find interface_body or class_body child
138        for i in 0..node.child_count() as u32 {
139            if let Some(child) = node.child(i) {
140                if child.kind() == "interface_body" || child.kind() == "class_body" {
141                    return Some(child);
142                }
143            }
144        }
145        None
146    }
147
148    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
149        false
150    }
151
152    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
153        let name_node = node.child_by_field_name("name")?;
154        Some(&content[name_node.byte_range()])
155    }
156
157    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
158        let ext = path.extension()?.to_str()?;
159        if !["ts", "mts", "cts", "tsx"].contains(&ext) {
160            return None;
161        }
162        let stem = path.with_extension("");
163        Some(stem.to_string_lossy().to_string())
164    }
165
166    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
167        vec![
168            format!("{}.ts", module),
169            format!("{}.tsx", module),
170            format!("{}/index.ts", module),
171        ]
172    }
173
174    fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
175        false
176    }
177
178    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
179        None
180    }
181
182    // === Import Resolution ===
183
184    fn lang_key(&self) -> &'static str {
185        "js"
186    } // Uses same cache as JS
187
188    fn resolve_local_import(
189        &self,
190        module: &str,
191        current_file: &Path,
192        _project_root: &Path,
193    ) -> Option<PathBuf> {
194        ecmascript::resolve_local_import(module, current_file, ecmascript::TS_EXTENSIONS)
195    }
196
197    fn resolve_external_import(
198        &self,
199        import_name: &str,
200        project_root: &Path,
201    ) -> Option<ResolvedPackage> {
202        ecmascript::resolve_external_import(import_name, project_root)
203    }
204
205    fn get_version(&self, _project_root: &Path) -> Option<String> {
206        ecmascript::get_version()
207    }
208
209    fn find_package_cache(&self, project_root: &Path) -> Option<PathBuf> {
210        ecmascript::find_package_cache(project_root)
211    }
212
213    fn indexable_extensions(&self) -> &'static [&'static str] {
214        &["ts", "mts", "cts", "js", "mjs", "cjs"]
215    }
216
217    fn package_sources(&self, project_root: &Path) -> Vec<crate::PackageSource> {
218        use crate::{PackageSource, PackageSourceKind};
219        let mut sources = Vec::new();
220        if let Some(cache) = self.find_package_cache(project_root) {
221            sources.push(PackageSource {
222                name: "node_modules",
223                path: cache,
224                kind: PackageSourceKind::NpmScoped,
225                version_specific: false,
226            });
227        }
228        sources
229    }
230
231    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
232        use crate::traits::{has_extension, skip_dotfiles};
233        if skip_dotfiles(name) {
234            return true;
235        }
236        if is_dir && (name == "node_modules" || name == ".bin" || name == "test" || name == "tests")
237        {
238            return true;
239        }
240        !is_dir && !has_extension(name, self.indexable_extensions())
241    }
242
243    fn package_module_name(&self, entry_name: &str) -> String {
244        for ext in &[".ts", ".mts", ".cts", ".d.ts", ".js", ".mjs", ".cjs"] {
245            if let Some(name) = entry_name.strip_suffix(ext) {
246                return name.to_string();
247            }
248        }
249        entry_name.to_string()
250    }
251
252    fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
253        self.discover_npm_scoped_packages(&source.path)
254    }
255
256    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
257        if path.is_file() {
258            return Some(path.to_path_buf());
259        }
260        ecmascript::find_package_entry(path)
261    }
262}
263
264// TSX shares the same implementation as TypeScript, just with a different grammar
265impl Language for Tsx {
266    fn name(&self) -> &'static str {
267        "TSX"
268    }
269    fn extensions(&self) -> &'static [&'static str] {
270        &["tsx"]
271    }
272    fn grammar_name(&self) -> &'static str {
273        "tsx"
274    }
275
276    fn has_symbols(&self) -> bool {
277        true
278    }
279
280    fn container_kinds(&self) -> &'static [&'static str] {
281        ecmascript::TS_CONTAINER_KINDS
282    }
283    fn function_kinds(&self) -> &'static [&'static str] {
284        ecmascript::TS_FUNCTION_KINDS
285    }
286    fn type_kinds(&self) -> &'static [&'static str] {
287        ecmascript::TS_TYPE_KINDS
288    }
289    fn import_kinds(&self) -> &'static [&'static str] {
290        ecmascript::IMPORT_KINDS
291    }
292    fn public_symbol_kinds(&self) -> &'static [&'static str] {
293        ecmascript::PUBLIC_SYMBOL_KINDS
294    }
295    fn visibility_mechanism(&self) -> VisibilityMechanism {
296        VisibilityMechanism::ExplicitExport
297    }
298    fn scope_creating_kinds(&self) -> &'static [&'static str] {
299        ecmascript::SCOPE_CREATING_KINDS
300    }
301    fn control_flow_kinds(&self) -> &'static [&'static str] {
302        ecmascript::CONTROL_FLOW_KINDS
303    }
304    fn complexity_nodes(&self) -> &'static [&'static str] {
305        ecmascript::COMPLEXITY_NODES
306    }
307    fn nesting_nodes(&self) -> &'static [&'static str] {
308        ecmascript::NESTING_NODES
309    }
310
311    fn signature_suffix(&self) -> &'static str {
312        " {}"
313    }
314
315    fn extract_function(&self, node: &Node, content: &str, in_container: bool) -> Option<Symbol> {
316        let name = self.node_name(node, content)?;
317        Some(ecmascript::extract_function(
318            node,
319            content,
320            in_container,
321            name,
322        ))
323    }
324
325    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
326        let name = self.node_name(node, content)?;
327        Some(ecmascript::extract_container(node, content, name))
328    }
329
330    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
331        let name = self.node_name(node, content)?;
332        ecmascript::extract_type(node, name)
333    }
334
335    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
336        ecmascript::extract_imports(node, content)
337    }
338
339    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
340        ecmascript::format_import(import, names)
341    }
342
343    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
344        ecmascript::extract_public_symbols(node, content)
345    }
346
347    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
348        None
349    }
350
351    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
352        Vec::new()
353    }
354
355    fn is_public(&self, _node: &Node, _content: &str) -> bool {
356        true
357    }
358
359    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
360        Visibility::Public
361    }
362
363    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
364        let name = symbol.name.as_str();
365        match symbol.kind {
366            crate::SymbolKind::Function | crate::SymbolKind::Method => {
367                name.starts_with("test_")
368                    || name.starts_with("Test")
369                    || name == "describe"
370                    || name == "it"
371                    || name == "test"
372            }
373            crate::SymbolKind::Module => name == "tests" || name == "test" || name == "__tests__",
374            _ => false,
375        }
376    }
377
378    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
379        None
380    }
381
382    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
383        // Try 'body' field first, then look for interface_body or class_body child
384        if let Some(body) = node.child_by_field_name("body") {
385            return Some(body);
386        }
387        // Fallback: find interface_body or class_body child
388        for i in 0..node.child_count() as u32 {
389            if let Some(child) = node.child(i) {
390                if child.kind() == "interface_body" || child.kind() == "class_body" {
391                    return Some(child);
392                }
393            }
394        }
395        None
396    }
397
398    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
399        false
400    }
401
402    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
403        let name_node = node.child_by_field_name("name")?;
404        Some(&content[name_node.byte_range()])
405    }
406
407    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
408        let ext = path.extension()?.to_str()?;
409        if ext != "tsx" {
410            return None;
411        }
412        let stem = path.with_extension("");
413        Some(stem.to_string_lossy().to_string())
414    }
415
416    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
417        vec![format!("{}.tsx", module), format!("{}/index.tsx", module)]
418    }
419
420    fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
421        false
422    }
423
424    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
425        None
426    }
427
428    // === Import Resolution ===
429
430    fn lang_key(&self) -> &'static str {
431        "js"
432    }
433
434    fn resolve_local_import(
435        &self,
436        module: &str,
437        current_file: &Path,
438        _project_root: &Path,
439    ) -> Option<PathBuf> {
440        ecmascript::resolve_local_import(module, current_file, ecmascript::TS_EXTENSIONS)
441    }
442
443    fn resolve_external_import(
444        &self,
445        import_name: &str,
446        project_root: &Path,
447    ) -> Option<ResolvedPackage> {
448        ecmascript::resolve_external_import(import_name, project_root)
449    }
450
451    fn get_version(&self, _project_root: &Path) -> Option<String> {
452        ecmascript::get_version()
453    }
454
455    fn find_package_cache(&self, project_root: &Path) -> Option<PathBuf> {
456        ecmascript::find_package_cache(project_root)
457    }
458
459    fn indexable_extensions(&self) -> &'static [&'static str] {
460        &["tsx", "ts", "js"]
461    }
462
463    fn package_sources(&self, project_root: &Path) -> Vec<crate::PackageSource> {
464        use crate::{PackageSource, PackageSourceKind};
465        let mut sources = Vec::new();
466        if let Some(cache) = self.find_package_cache(project_root) {
467            sources.push(PackageSource {
468                name: "node_modules",
469                path: cache,
470                kind: PackageSourceKind::NpmScoped,
471                version_specific: false,
472            });
473        }
474        sources
475    }
476
477    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
478        use crate::traits::{has_extension, skip_dotfiles};
479        if skip_dotfiles(name) {
480            return true;
481        }
482        if is_dir && (name == "node_modules" || name == ".bin" || name == "test" || name == "tests")
483        {
484            return true;
485        }
486        !is_dir && !has_extension(name, self.indexable_extensions())
487    }
488
489    fn package_module_name(&self, entry_name: &str) -> String {
490        for ext in &[".tsx", ".ts", ".d.ts", ".js"] {
491            if let Some(name) = entry_name.strip_suffix(ext) {
492                return name.to_string();
493            }
494        }
495        entry_name.to_string()
496    }
497
498    fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
499        self.discover_npm_scoped_packages(&source.path)
500    }
501
502    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
503        if path.is_file() {
504            return Some(path.to_path_buf());
505        }
506        ecmascript::find_package_entry(path)
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::validate_unused_kinds_audit;
514
515    /// Documents node kinds that exist in the TypeScript grammar but aren't used in trait methods.
516    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
517    #[test]
518    fn unused_node_kinds_audit() {
519        #[rustfmt::skip]
520        let documented_unused: &[&str] = &[
521            // STRUCTURAL
522            "class_body",              // class body block
523            "class_heritage",          // extends clause
524            "class_static_block",      // static { }
525            "enum_assignment",         // enum value assignment
526            "enum_body",               // enum body
527            "formal_parameters",       // function params
528            "identifier",              // too common
529            "interface_body",          // interface body
530            "nested_identifier",       // a.b.c path
531            "nested_type_identifier",  // a.b.Type path
532            "private_property_identifier", // #field
533            "property_identifier",     // obj.prop
534            "public_field_definition", // class field
535            "shorthand_property_identifier", // { x } shorthand
536            "shorthand_property_identifier_pattern", // destructuring
537            "statement_block",         // { }
538            "statement_identifier",    // label name
539            "switch_body",             // switch cases
540
541            // CLAUSE
542            "default_type",            // default type param
543            "else_clause",             // else branch
544            "extends_clause",          // class extends
545            "extends_type_clause",     // T extends U
546            "finally_clause",          // finally block
547            "implements_clause",       // implements X
548
549            // EXPRESSION
550            "as_expression",           // x as T
551            "assignment_expression",   // x = y
552            "augmented_assignment_expression", // x += y
553            "await_expression",        // await foo
554            "call_expression",         // foo()
555            "function_expression",     // function() {}
556            "instantiation_expression",// generic call
557            "member_expression",       // foo.bar
558            "new_expression",          // new Foo()
559            "non_null_expression",     // x!
560            "parenthesized_expression",// (expr)
561            "satisfies_expression",    // x satisfies T
562            "sequence_expression",     // a, b
563            "subscript_expression",    // arr[i]
564            "unary_expression",        // -x, !x
565            "update_expression",       // x++
566            "yield_expression",        // yield x
567
568            // TYPE NODES
569            "adding_type_annotation",  // : T
570            "array_type",              // T[]
571            "conditional_type",        // T extends U ? V : W
572            "construct_signature",     // new(): T
573            "constructor_type",        // new (x: T) => U
574            "existential_type",        // *
575            "flow_maybe_type",         // ?T
576            "function_signature",      // function sig
577            "function_type",           // (x: T) => U
578            "generic_type",            // T<U>
579            "index_type_query",        // keyof T
580            "infer_type",              // infer T
581            "intersection_type",       // T & U
582            "literal_type",            // "foo" type
583            "lookup_type",             // T[K]
584            "mapped_type_clause",      // [K in T]
585            "object_type",             // { x: T }
586            "omitting_type_annotation",// omit annotation
587            "opting_type_annotation",  // optional annotation
588            "optional_type",           // T?
589            "override_modifier",       // override
590            "parenthesized_type",      // (T)
591            "predefined_type",         // string, number
592            "readonly_type",           // readonly T
593            "rest_type",               // ...T
594            "template_literal_type",   // `${T}`
595            "template_type",           // template type
596            "this_type",               // this
597            "tuple_type",              // [T, U]
598            "type_annotation",         // : T
599            "type_arguments",          // <T, U>
600            "type_assertion",          // <T>x
601            "type_identifier",         // type name
602            "type_parameter",          // T
603            "type_parameters",         // <T, U>
604            "type_predicate",          // x is T
605            "type_predicate_annotation", // : x is T
606            "type_query",              // typeof x
607            "union_type",              // T | U
608
609            // IMPORT/EXPORT DETAILS
610            "accessibility_modifier",  // public/private/protected
611            "export_clause",           // export { a, b }
612            "export_specifier",        // export { a as b }
613            "import",                  // import keyword
614            "import_alias",            // import X = Y
615            "import_attribute",        // import attributes
616            "import_clause",           // import clause
617            "import_require_clause",   // require()
618            "import_specifier",        // import { a }
619            "named_imports",           // { a, b }
620            "namespace_export",        // export * as ns
621            "namespace_import",        // import * as ns
622
623            // DECLARATION
624            "abstract_class_declaration", // abstract class
625            "abstract_method_signature", // abstract method
626            "ambient_declaration",     // declare
627            "debugger_statement",      // debugger;
628            "empty_statement",         // ;
629            "expression_statement",    // expr;
630            "generator_function",      // function* foo
631            "generator_function_declaration", // function* declaration
632            "internal_module",         // namespace/module
633            "labeled_statement",       // label: stmt
634            "lexical_declaration",     // let/const
635            "module",                  // module keyword
636            "using_declaration",       // using x = ...
637            "variable_declaration",    // var x
638            "with_statement",          // with (obj) - deprecated
639        ];
640
641        validate_unused_kinds_audit(&TypeScript, documented_unused)
642            .expect("TypeScript unused node kinds audit failed");
643    }
644}