use crate::language::DefaultVisibility;
use crate::language::LangId;
use crate::language::pack::define_language_pack;
use crate::model::Visibility;
use std::path::{Path, PathBuf};
fn resolve_ts_import(raw: &str, source_dir: &Path, _project_root: &Path) -> Option<PathBuf> {
use crate::language::import_resolver::{TS_EXTS, resolve_js_family_import};
resolve_js_family_import(raw, source_dir, TS_EXTS, &|p| p.is_file())
}
pub(crate) const TS_FAMILY_QUERY: &str = r#"
(function_declaration
"async"? @async
name: (identifier) @name
parameters: (formal_parameters) @signature
) @kind.function
(generator_function_declaration
"async"? @async
name: (identifier) @name
parameters: (formal_parameters) @signature
) @kind.function
(class_declaration
(type_identifier) @name
) @kind.class
(abstract_class_declaration
(type_identifier) @name
) @kind.class
(interface_declaration
(type_identifier) @name
) @kind.interface
(enum_declaration
(identifier) @name
) @kind.enum
(type_alias_declaration
(type_identifier) @name
) @kind.type_alias
(method_definition
"async"? @async
name: (_) @name
parameters: (formal_parameters) @signature
) @kind.method
(export_statement
[
(function_declaration
"async"? @async
name: (identifier) @name
parameters: (formal_parameters) @signature
) @kind.function
(class_declaration
(type_identifier) @name
) @kind.class
(abstract_class_declaration
(type_identifier) @name
) @kind.class
(interface_declaration
(type_identifier) @name
) @kind.interface
(enum_declaration
(identifier) @name
) @kind.enum
(type_alias_declaration
(type_identifier) @name
) @kind.type_alias
]
)
(variable_declarator
name: (identifier) @name
value: (arrow_function
"async"? @async
parameters: (formal_parameters) @signature)
) @kind.function
(variable_declarator
name: (identifier) @name
value: (function_expression
"async"? @async
parameters: (formal_parameters) @signature)
) @kind.function
"#;
pub(crate) const TS_FAMILY_IMPORT_QUERY: &str = r#"
(import_statement
source: (string) @import.path)
(import_statement
(import_clause
(named_imports
(import_specifier
name: (identifier) @import.symbol
alias: (identifier)? @import.alias))))
(import_statement
(import_clause
(identifier) @import.symbol))
(import_statement
(import_clause
(namespace_import
(identifier) @import.symbol)))
(call_expression
function: (identifier) @call.name
arguments: (arguments . (string) @import.path .)
(#eq? @call.name "require"))
"#;
pub(crate) const TS_FAMILY_REFERENCE_QUERY: &str = r#"
(call_expression
function: (identifier) @reference.name)
(call_expression
function: (member_expression
property: (property_identifier) @reference.name))
(call_expression
function: (member_expression
object: (identifier) @reference.name))
"#;
define_language_pack!(
spec: TS_SPEC,
label: "TypeScript",
lang: LangId::TypeScript,
grammar: tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
extensions: ["ts", "cts", "mts"],
resolver: resolve_ts_import,
symbols: {
static: TS_QUERY,
accessor: ts_query,
query: TS_FAMILY_QUERY,
},
imports_refs: {
static: TS_IMPORT_REF_QUERY,
accessor: ts_import_ref_query,
import: TS_FAMILY_IMPORT_QUERY,
reference: TS_FAMILY_REFERENCE_QUERY,
},
dataflow: {
static: TS_DATAFLOW_QUERY,
accessor: ts_dataflow_query,
query: crate::language::javascript::TS_FAMILY_DATAFLOW_QUERY,
},
import_statement_kinds: ["import_statement"],
class_like_parents: ["class_declaration", "class"],
ancestors: [("export_statement", Visibility::Public)],
visibility_from_name: None,
default_visibility: DefaultVisibility::PrivateByDefault,
doc_comment: Some(crate::language::C_LIKE_DOC_COMMENT),
fixture: "tests/fixtures/typescript/interfaces.ts",
snapshot: ts_insta_snapshot,
tests {
use crate::model::SymbolKind;
#[test]
fn extract_interface() {
let src = b"interface Foo { bar(): void; }";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "Foo");
assert!(matches!(symbols[0].kind, SymbolKind::Interface));
}
#[test]
fn extract_type_alias() {
let src = b"type Point = { x: number; };";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "Point");
assert!(matches!(symbols[0].kind, SymbolKind::TypeAlias));
}
#[test]
fn extract_enum() {
let src = b"enum Dir { A, B }";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "Dir");
assert!(matches!(symbols[0].kind, SymbolKind::Enum));
}
#[test]
fn extract_ts_named_imports() {
use crate::language::extract_imports_and_references_for;
let src = b"import { Component, OnInit } from '@angular/core';";
let tree = parse(src);
let (imports, _, _) = extract_imports_and_references_for(
LangId::TypeScript,
&tree,
src,
&std::path::PathBuf::from("test.ts"),
);
let named: Vec<_> = imports.iter().filter(|i| i.symbol.is_some()).collect();
assert_eq!(named.len(), 2);
for imp in &named {
assert_eq!(imp.import_specifier, "@angular/core");
}
assert_eq!(named[0].symbol.as_deref(), Some("Component"));
assert_eq!(named[1].symbol.as_deref(), Some("OnInit"));
}
#[test]
fn extract_ts_default_import() {
use crate::language::extract_imports_and_references_for;
let src = b"import React from 'react';";
let tree = parse(src);
let (imports, _, _) = extract_imports_and_references_for(
LangId::TypeScript,
&tree,
src,
&std::path::PathBuf::from("test.ts"),
);
let named: Vec<_> = imports.iter().filter(|i| i.symbol.is_some()).collect();
assert_eq!(named.len(), 1);
assert_eq!(named[0].import_specifier, "react");
assert_eq!(named[0].symbol.as_deref(), Some("React"));
}
#[test]
fn ts_docstring_extraction() {
let src = b"/** TSDoc comment. */\nfunction documented() {}";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
let func = symbols.iter().find(|s| s.name == "documented").unwrap();
assert!(func.docstring.is_some(), "documented should have docstring");
assert!(func.docstring.as_ref().unwrap().contains("TSDoc comment"));
}
fn symbol_names(symbols: &[crate::language::RawSymbol<'_>]) -> Vec<String> {
symbols.iter().map(|s| s.name.to_string()).collect()
}
#[test]
fn extract_annotated_arrow_function() {
let src = b"const f: T = () => {};";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
let found = symbols.iter().find(|s| s.name == "f");
assert!(found.is_some(), "missing f: {:?}", symbol_names(&symbols));
let found = found.unwrap();
assert!(matches!(found.kind, SymbolKind::Function));
assert_eq!(found.signature.as_deref(), Some("()"));
}
#[test]
fn extract_exported_annotated_arrow_function() {
let src = b"export const handler: Handler = async () => {};";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
let found = symbols.iter().find(|s| s.name == "handler");
assert!(
found.is_some(),
"missing handler: {:?}",
symbol_names(&symbols)
);
let found = found.unwrap();
assert!(matches!(found.kind, SymbolKind::Function));
assert!(found.is_async);
}
#[test]
fn extract_typed_function_expression() {
let src = b"const g = function n(): void {};";
let tree = parse(src);
let symbols = extract_symbols_for(LangId::TypeScript, &tree, src);
assert!(
symbols.iter().any(|s| s.name == "g"),
"missing g: {:?}",
symbol_names(&symbols)
);
assert!(!symbols.iter().any(|s| s.name == "n"));
}
#[cfg(feature = "dataflow")]
mod dataflow_tests {
use super::*;
use crate::language::typescript::extract_typescript_dataflow;
use crate::model::{DataScope, FlowKind};
fn extract(source: &[u8]) -> (Vec<crate::model::DataNode>, Vec<crate::model::FlowEdge>) {
let id_gen = crate::model::IdGenerator::new();
extract_typescript_dataflow(&parse(source), source, &id_gen)
}
#[test]
fn typed_let_binding_captured() {
let src = b"function f(): number { let x: number = 42; return x; }";
let (nodes, edges) = extract(src);
assert!(
nodes
.iter()
.any(|n| n.name.as_deref() == Some("x") && n.scope == DataScope::Local)
);
assert!(!edges.is_empty(), "x usage should yield an edge");
}
#[test]
fn typed_parameter_captured_as_parameter() {
let src = b"function add(a: number, b: number): number { return a + b; }";
let (nodes, edges) = extract(src);
let params: Vec<_> = nodes
.iter()
.filter(|n| n.scope == DataScope::Parameter)
.collect();
assert_eq!(params.len(), 2);
let names: Vec<_> = params.iter().map(|n| n.name.as_deref()).collect();
assert!(names.contains(&Some("a")));
assert!(names.contains(&Some("b")));
assert!(!edges.is_empty());
}
#[test]
fn flow_edges_anchored_to_real_nodes() {
let src = b"function f() { let x = 1; return x; }";
let (nodes, edges) = extract(src);
let ids: std::collections::HashSet<_> = nodes.iter().map(|n| n.id).collect();
for edge in &edges {
assert!(ids.contains(&edge.source), "edge source not in nodes");
assert!(ids.contains(&edge.target), "edge target not in nodes");
assert_eq!(edge.kind, FlowKind::DefUse);
assert!((edge.confidence - 0.9).abs() < f32::EPSILON);
}
}
#[test]
fn no_duplicate_def_for_typed_let() {
let src = b"function f() { let x: number = 1; return x; }";
let (nodes, _edges) = extract(src);
let x_defs: Vec<_> = nodes
.iter()
.filter(|n| n.name.as_deref() == Some("x") && n.scope == DataScope::Local)
.collect();
assert_eq!(
x_defs.len(),
2,
"expected exactly one def + one use for `x` (no double-capture)"
);
}
#[test]
fn cross_function_scoping() {
let src = b"function outer() { let x = 1; function inner() { let x = 2; return x; } return x; }";
let (nodes, edges) = extract(src);
let defs: Vec<_> = nodes
.iter()
.filter(|n| n.name.as_deref() == Some("x") && n.scope == DataScope::Local)
.collect();
assert_eq!(defs.len(), 4);
let true_defs: Vec<_> = nodes
.iter()
.filter(|n| {
n.name.as_deref() == Some("x")
&& n.scope == DataScope::Local
&& !edges.iter().any(|e| e.target == n.id)
})
.collect();
assert_eq!(true_defs.len(), 2, "two distinct `x` defs expected");
assert!(edges.len() >= 2);
let inner_def = true_defs
.iter()
.max_by_key(|n| n.source_range.byte_start)
.unwrap();
let outer_def = true_defs
.iter()
.min_by_key(|n| n.source_range.byte_start)
.unwrap();
let uses: Vec<_> = nodes
.iter()
.filter(|n| {
n.name.as_deref() == Some("x")
&& n.scope == DataScope::Local
&& edges.iter().any(|e| e.target == n.id)
})
.collect();
let inner_use = uses
.iter()
.min_by_key(|n| n.source_range.byte_start)
.unwrap();
let outer_use = uses
.iter()
.max_by_key(|n| n.source_range.byte_start)
.unwrap();
let inner_edge = edges.iter().find(|e| e.target == inner_use.id).unwrap();
let outer_edge = edges.iter().find(|e| e.target == outer_use.id).unwrap();
assert_eq!(inner_edge.source, inner_def.id);
assert_eq!(outer_edge.source, outer_def.id);
assert_ne!(inner_edge.source, outer_edge.source);
}
#[test]
fn dataflow_against_fixture_file() {
let src = std::fs::read_to_string(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/typescript/interfaces.ts"),
)
.unwrap();
let (nodes, edges) = extract(src.as_bytes());
assert!(!nodes.is_empty(), "fixture must yield some data nodes");
let ids: std::collections::HashSet<_> = nodes.iter().map(|n| n.id).collect();
for edge in &edges {
assert!(ids.contains(&edge.source));
assert!(ids.contains(&edge.target));
}
}
}
},
);
#[cfg(feature = "dataflow")]
pub(crate) const TS_FUNCTION_KINDS: &[&str] = crate::language::common::JS_FAMILY_FUNCTION_KINDS;
#[cfg(feature = "dataflow")]
pub fn extract_typescript_dataflow(
tree: &tree_sitter::Tree,
source: &[u8],
id_gen: &crate::model::IdGenerator<crate::model::DataNodeId>,
) -> (Vec<crate::model::DataNode>, Vec<crate::model::FlowEdge>) {
let Some(query) = ts_dataflow_query() else {
return (Vec::new(), Vec::new());
};
crate::language::javascript::extract_js_family_dataflow_with_query(
tree,
source,
query,
TS_FUNCTION_KINDS,
id_gen,
)
}