use tokensave::extraction::LanguageExtractor;
use tokensave::extraction::TypeScriptExtractor;
use tokensave::types::*;
#[test]
fn test_ts_file_node_is_root() {
let source = r#"function main() {}"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("test.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let files: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::File)
.collect();
assert_eq!(files.len(), 1);
assert_eq!(files[0].name, "test.ts");
assert_eq!(files[0].visibility, Visibility::Pub);
}
#[test]
fn test_ts_function_declaration() {
let source = r#"
function add(a: number, b: number): number {
return a + b;
}
function helper(): void {}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("math.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert_eq!(fns.len(), 2);
let add_fn = fns.iter().find(|f| f.name == "add").unwrap();
assert_eq!(add_fn.visibility, Visibility::Private); let helper_fn = fns.iter().find(|f| f.name == "helper").unwrap();
assert_eq!(helper_fn.visibility, Visibility::Private);
}
#[test]
fn test_ts_exported_function_is_pub() {
let source = r#"
export function greet(name: string): string {
return "Hello, " + name;
}
function internal(): void {}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("greet.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert_eq!(fns.len(), 2);
let greet_fn = fns.iter().find(|f| f.name == "greet").unwrap();
assert_eq!(greet_fn.visibility, Visibility::Pub);
let internal_fn = fns.iter().find(|f| f.name == "internal").unwrap();
assert_eq!(internal_fn.visibility, Visibility::Private);
}
#[test]
fn test_ts_arrow_function() {
let source = r#"
const add = (a: number, b: number): number => a + b;
export const multiply = (a: number, b: number) => {
return a * b;
};
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("arrow.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let arrows: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::ArrowFunction)
.collect();
assert_eq!(arrows.len(), 2);
let add = arrows.iter().find(|f| f.name == "add").unwrap();
assert_eq!(add.visibility, Visibility::Private);
let multiply = arrows.iter().find(|f| f.name == "multiply").unwrap();
assert_eq!(multiply.visibility, Visibility::Pub);
}
#[test]
fn test_ts_class_with_methods() {
let source = r#"
export class MyClass {
private name: string;
public age: number;
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
private helper(): void {}
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("class.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Class)
.collect();
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "MyClass");
assert_eq!(classes[0].visibility, Visibility::Pub);
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Method)
.collect();
assert_eq!(methods.len(), 2); let get_name = methods.iter().find(|m| m.name == "getName").unwrap();
assert_eq!(get_name.visibility, Visibility::Pub); let helper = methods.iter().find(|m| m.name == "helper").unwrap();
assert_eq!(helper.visibility, Visibility::Private);
let constructors: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Constructor)
.collect();
assert_eq!(constructors.len(), 1);
let fields: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Field)
.collect();
assert_eq!(fields.len(), 2);
let name_field = fields.iter().find(|f| f.name == "name").unwrap();
assert_eq!(name_field.visibility, Visibility::Private);
let age_field = fields.iter().find(|f| f.name == "age").unwrap();
assert_eq!(age_field.visibility, Visibility::Pub);
let class_id = &classes[0].id;
let contains_edges: Vec<_> = result
.edges
.iter()
.filter(|e| e.source == *class_id && e.kind == EdgeKind::Contains)
.collect();
assert_eq!(contains_edges.len(), 5);
}
#[test]
fn test_ts_interface() {
let source = r#"
export interface Printable {
print(): void;
toString(): string;
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("iface.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let interfaces: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Interface)
.collect();
assert_eq!(interfaces.len(), 1);
assert_eq!(interfaces[0].name, "Printable");
assert_eq!(interfaces[0].visibility, Visibility::Pub);
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Method)
.collect();
assert_eq!(methods.len(), 2);
}
#[test]
fn test_ts_enum() {
let source = r#"
export enum Color {
Red,
Green,
Blue
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("color.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let enums: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Enum)
.collect();
assert_eq!(enums.len(), 1);
assert_eq!(enums[0].name, "Color");
assert_eq!(enums[0].visibility, Visibility::Pub);
let variants: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::EnumVariant)
.collect();
assert_eq!(variants.len(), 3);
assert!(variants.iter().any(|v| v.name == "Red"));
assert!(variants.iter().any(|v| v.name == "Green"));
assert!(variants.iter().any(|v| v.name == "Blue"));
}
#[test]
fn test_ts_import_export() {
let source = r#"
import { foo, bar } from './utils';
import * as path from 'path';
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("imports.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let uses: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Use)
.collect();
assert_eq!(uses.len(), 2);
assert!(uses.iter().any(|u| u.name == "./utils"));
assert!(uses.iter().any(|u| u.name == "path"));
let use_refs: Vec<_> = result
.unresolved_refs
.iter()
.filter(|r| r.reference_kind == EdgeKind::Uses)
.collect();
assert_eq!(use_refs.len(), 2);
}
#[test]
fn test_ts_async_function() {
let source = r#"
export async function fetchData(url: string): Promise<string> {
const response = await fetch(url);
return response.text();
}
function syncHelper(): void {}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("async.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
let fetch_fn = fns.iter().find(|f| f.name == "fetchData").unwrap();
assert!(fetch_fn.is_async, "fetchData should be async");
let sync_fn = fns.iter().find(|f| f.name == "syncHelper").unwrap();
assert!(!sync_fn.is_async, "syncHelper should not be async");
}
#[test]
fn test_ts_decorator() {
let source = r#"
@Injectable()
class Service {
getData(): string { return "data"; }
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("service.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let decorators: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Decorator)
.collect();
assert_eq!(decorators.len(), 1);
assert_eq!(decorators[0].name, "Injectable");
let annotates: Vec<_> = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Annotates)
.collect();
assert_eq!(annotates.len(), 1);
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Class)
.collect();
assert_eq!(annotates[0].target, classes[0].id);
}
#[test]
fn test_ts_namespace() {
let source = r#"
namespace MyNamespace {
export function inner(): void {}
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("ns.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let namespaces: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Namespace)
.collect();
assert_eq!(namespaces.len(), 1);
assert_eq!(namespaces[0].name, "MyNamespace");
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert_eq!(fns.len(), 1);
assert_eq!(fns[0].name, "inner");
assert_eq!(fns[0].visibility, Visibility::Pub); }
#[test]
fn test_ts_jsdoc_docstring() {
let source = r#"
/** Adds two numbers together. */
function add(a: number, b: number): number {
return a + b;
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("doc.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert_eq!(fns.len(), 1);
let docstring = fns[0].docstring.as_ref().expect("should have docstring");
assert!(
docstring.contains("Adds two numbers together"),
"docstring should contain the JSDoc text, got: {docstring}"
);
}
#[test]
fn test_ts_jsdoc_on_exported_function() {
let source = r#"
/** Greets someone by name. */
export function greet(name: string): string {
return "Hello, " + name;
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("doc_export.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert_eq!(fns.len(), 1);
let docstring = fns[0]
.docstring
.as_ref()
.expect("exported function should have docstring");
assert!(
docstring.contains("Greets someone by name"),
"docstring should contain the JSDoc text, got: {docstring}"
);
}
#[test]
fn test_ts_call_site_tracking() {
let source = r#"
function greet(name: string): void {
console.log("Hello", name);
}
function main(): void {
greet("world");
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("calls.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let call_refs: Vec<_> = result
.unresolved_refs
.iter()
.filter(|r| r.reference_kind == EdgeKind::Calls)
.collect();
assert!(!call_refs.is_empty(), "should have call refs");
assert!(
call_refs.iter().any(|r| r.reference_name.contains("greet")),
"should have a call to greet"
);
assert!(
call_refs
.iter()
.any(|r| r.reference_name.contains("console.log")),
"should have a call to console.log"
);
}
#[test]
fn test_ts_type_alias() {
let source = r#"
export type StringOrNum = string | number;
type ID = string;
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("types.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let aliases: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::TypeAlias)
.collect();
assert_eq!(aliases.len(), 2);
let son = aliases.iter().find(|a| a.name == "StringOrNum").unwrap();
assert_eq!(son.visibility, Visibility::Pub);
let id_type = aliases.iter().find(|a| a.name == "ID").unwrap();
assert_eq!(id_type.visibility, Visibility::Private);
}
#[test]
fn test_ts_class_extends_implements() {
let source = r#"
interface Printable {
print(): void;
}
class Base {}
export class Child extends Base implements Printable {
print(): void {}
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("inherit.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let extends_refs: Vec<_> = result
.unresolved_refs
.iter()
.filter(|r| r.reference_kind == EdgeKind::Extends)
.collect();
assert!(
!extends_refs.is_empty(),
"should have Extends ref for Base"
);
assert!(extends_refs.iter().any(|r| r.reference_name == "Base"));
let impl_refs: Vec<_> = result
.unresolved_refs
.iter()
.filter(|r| r.reference_kind == EdgeKind::Implements)
.collect();
assert!(
!impl_refs.is_empty(),
"should have Implements ref for Printable"
);
assert!(impl_refs.iter().any(|r| r.reference_name == "Printable"));
}
#[test]
fn test_ts_contains_edges() {
let source = r#"
function foo(): void {}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("edges.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let file_node = result
.nodes
.iter()
.find(|n| n.kind == NodeKind::File)
.unwrap();
let fn_node = result
.nodes
.iter()
.find(|n| n.kind == NodeKind::Function)
.unwrap();
let contains = result
.edges
.iter()
.find(|e| e.source == file_node.id && e.target == fn_node.id);
assert!(
contains.is_some(),
"File should contain the function via Contains edge"
);
assert_eq!(contains.unwrap().kind, EdgeKind::Contains);
}
#[test]
fn test_js_file_uses_js_grammar() {
let source = r#"
/** Adds two numbers */
function add(a, b) {
return a + b;
}
export default class Foo {
bar() { return 1; }
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("test.js", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert_eq!(fns.len(), 1);
assert_eq!(fns[0].name, "add");
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Class)
.collect();
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "Foo");
assert_eq!(classes[0].visibility, Visibility::Pub);
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Method)
.collect();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "bar");
}
#[test]
fn test_ts_jsx_file() {
let source = r#"
import React from 'react';
export function App() {
return <div>Hello</div>;
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("app.jsx", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let fns: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
assert!(
fns.iter().any(|f| f.name == "App"),
"should extract App function from JSX"
);
}
#[test]
fn test_ts_tsx_file() {
let source = r#"
import React from 'react';
interface Props {
name: string;
}
export const Greeting: React.FC<Props> = ({ name }) => {
return <div>Hello, {name}</div>;
};
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("greeting.tsx", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let interfaces: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Interface)
.collect();
assert_eq!(interfaces.len(), 1);
assert_eq!(interfaces[0].name, "Props");
let arrows: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::ArrowFunction)
.collect();
assert_eq!(arrows.len(), 1);
assert_eq!(arrows[0].name, "Greeting");
assert_eq!(arrows[0].visibility, Visibility::Pub);
}
#[test]
fn test_ts_const_declaration() {
let source = r#"
export const MAX_SIZE = 1024;
const SECRET = "hidden";
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("consts.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let consts: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Const)
.collect();
assert_eq!(consts.len(), 2);
let max = consts.iter().find(|c| c.name == "MAX_SIZE").unwrap();
assert_eq!(max.visibility, Visibility::Pub);
let secret = consts.iter().find(|c| c.name == "SECRET").unwrap();
assert_eq!(secret.visibility, Visibility::Private);
}
#[test]
fn test_ts_async_arrow_function() {
let source = r#"
const fetchData = async (url: string) => {
return await fetch(url);
};
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("async_arrow.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let arrows: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::ArrowFunction)
.collect();
assert_eq!(arrows.len(), 1);
assert_eq!(arrows[0].name, "fetchData");
assert!(arrows[0].is_async, "fetchData arrow should be async");
}
#[test]
fn test_ts_multiple_decorators() {
let source = r#"
@Component({
selector: 'app-root'
})
@Injectable()
class AppComponent {}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("decorators.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let decorators: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Decorator)
.collect();
assert_eq!(decorators.len(), 2);
assert!(decorators.iter().any(|d| d.name == "Component"));
assert!(decorators.iter().any(|d| d.name == "Injectable"));
}
#[test]
fn test_ts_enum_private() {
let source = r#"
enum Direction {
Up,
Down,
Left,
Right
}
"#;
let extractor = TypeScriptExtractor;
let result = extractor.extract("dir.ts", source);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
let enums: Vec<_> = result
.nodes
.iter()
.filter(|n| n.kind == NodeKind::Enum)
.collect();
assert_eq!(enums.len(), 1);
assert_eq!(enums[0].visibility, Visibility::Private); }