use tree_sitter::{Node, Tree};
use crate::index::format::{ReferenceEntry, SymbolEntry, TextEntry};
use crate::parser::helpers::*;
use crate::parser::treesitter::MAX_DEPTH;
const JS_STOPWORDS: &[&str] = &[
"undefined",
"null",
"console",
"window",
"document",
"exports",
"module",
"require",
"import",
"export",
"from",
"let",
"var",
"function",
"extends",
"finally",
"async",
"await",
"yield",
"typeof",
"instanceof",
"delete",
"of",
"prototype",
"constructor",
"length",
"name",
"arguments",
"callee",
"caller",
];
fn filter_js_tokens(tokens: Option<String>) -> Option<String> {
tokens.and_then(|t| {
let filtered: Vec<&str> = t
.split_whitespace()
.filter(|tok| !JS_STOPWORDS.contains(&tok.to_lowercase().as_str()))
.collect();
if filtered.is_empty() {
None
} else {
Some(filtered.join(" "))
}
})
}
pub fn extract(
tree: &Tree,
source: &[u8],
file_path: &str,
symbols: &mut Vec<SymbolEntry>,
texts: &mut Vec<TextEntry>,
references: &mut Vec<ReferenceEntry>,
) {
let root = tree.root_node();
walk_node(root, source, file_path, None, symbols, texts, references, 0);
}
fn is_js_builtin_call(name: &str) -> bool {
matches!(
name,
"console"
| "log"
| "error"
| "warn"
| "info"
| "debug"
| "trace"
| "dir"
| "table"
| "time"
| "timeEnd"
| "clear"
| "count"
| "group"
| "groupEnd"
| "assert"
| "parseInt"
| "parseFloat"
| "isNaN"
| "isFinite"
| "encodeURI"
| "decodeURI"
| "encodeURIComponent"
| "decodeURIComponent"
| "eval"
| "setTimeout"
| "setInterval"
| "clearTimeout"
| "clearInterval"
| "fetch"
| "require"
| "alert"
| "confirm"
| "prompt"
| "Object"
| "Array"
| "String"
| "Number"
| "Boolean"
| "Symbol"
| "BigInt"
| "Date"
| "RegExp"
| "Error"
| "Map"
| "Set"
| "WeakMap"
| "WeakSet"
| "Promise"
| "Proxy"
| "Reflect"
| "JSON"
| "Math"
| "Function"
| "push"
| "pop"
| "shift"
| "unshift"
| "slice"
| "splice"
| "concat"
| "join"
| "reverse"
| "sort"
| "filter"
| "map"
| "reduce"
| "reduceRight"
| "forEach"
| "find"
| "findIndex"
| "indexOf"
| "includes"
| "every"
| "some"
| "flat"
| "flatMap"
| "keys"
| "values"
| "entries"
| "from"
| "of"
| "isArray"
| "charAt"
| "charCodeAt"
| "substring"
| "substr"
| "replace"
| "replaceAll"
| "split"
| "toLowerCase"
| "toUpperCase"
| "trim"
| "trimStart"
| "trimEnd"
| "padStart"
| "padEnd"
| "repeat"
| "startsWith"
| "endsWith"
| "match"
| "search"
| "hasOwnProperty"
| "toString"
| "valueOf"
| "assign"
| "create"
| "defineProperty"
| "freeze"
| "seal"
| "getPrototypeOf"
| "setPrototypeOf"
| "then"
| "catch"
| "finally"
| "resolve"
| "reject"
| "all"
| "race"
| "allSettled"
| "any"
| "abs"
| "ceil"
| "floor"
| "round"
| "max"
| "min"
| "pow"
| "sqrt"
| "random"
| "sin"
| "cos"
| "parse"
| "stringify"
| "describe"
| "it"
| "test"
| "expect"
| "beforeEach"
| "afterEach"
| "beforeAll"
| "afterAll"
| "jest"
| "mock"
| "spyOn"
| "toBe"
| "toEqual"
)
}
#[allow(clippy::too_many_arguments)]
fn walk_node(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
symbols: &mut Vec<SymbolEntry>,
texts: &mut Vec<TextEntry>,
references: &mut Vec<ReferenceEntry>,
depth: usize,
) {
if depth > MAX_DEPTH {
return;
}
let kind = node.kind();
match kind {
"function_declaration" => {
extract_function_decl(node, source, file_path, parent_ctx, symbols);
}
"generator_function_declaration" => {
extract_function_decl(node, source, file_path, parent_ctx, symbols);
}
"class_declaration" => {
extract_class(
node, source, file_path, parent_ctx, symbols, texts, references, depth,
);
return; }
"method_definition" => {
extract_method(node, source, file_path, parent_ctx, symbols);
}
"lexical_declaration" | "variable_declaration" => {
extract_variable_decl(node, source, file_path, parent_ctx, symbols);
}
"export_statement" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_node(
child,
source,
file_path,
parent_ctx,
symbols,
texts,
references,
depth + 1,
);
}
return;
}
"import_statement" => {
extract_import(node, source, file_path, symbols, references);
}
"call_expression" => {
extract_call_ref(node, source, file_path, parent_ctx, references);
}
"new_expression" => {
extract_new_ref(node, source, file_path, parent_ctx, references);
}
"comment" => {
extract_js_comment(node, source, file_path, parent_ctx, texts);
return;
}
"string" | "template_string" => {
extract_string(node, source, file_path, parent_ctx, texts);
return;
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_node(
child,
source,
file_path,
parent_ctx,
symbols,
texts,
references,
depth + 1,
);
}
}
fn extract_call_ref(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
references: &mut Vec<ReferenceEntry>,
) {
let func = match find_child_by_field(node, "function") {
Some(f) => f,
None => return,
};
let name = get_call_name(func, source);
if name.is_empty() || is_js_builtin_call(&name) {
return;
}
let line = node_line_range(node);
references.push(ReferenceEntry {
file: file_path.to_string(),
name,
kind: "call".to_string(),
line,
caller: parent_ctx.map(String::from),
project: String::new(),
});
}
fn extract_new_ref(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
references: &mut Vec<ReferenceEntry>,
) {
let constructor = match find_child_by_field(node, "constructor") {
Some(c) => c,
None => return,
};
let name = get_call_name(constructor, source);
if name.is_empty() || is_js_builtin_call(&name) {
return;
}
let line = node_line_range(node);
references.push(ReferenceEntry {
file: file_path.to_string(),
name,
kind: "instantiation".to_string(),
line,
caller: parent_ctx.map(String::from),
project: String::new(),
});
}
fn get_call_name(node: Node, source: &[u8]) -> String {
match node.kind() {
"identifier" => node_text(node, source),
"member_expression" => {
if let Some(prop) = find_child_by_field(node, "property") {
if let Some(obj) = find_child_by_field(node, "object") {
let obj_name = get_call_name(obj, source);
let prop_name = node_text(prop, source);
if obj_name.is_empty() {
prop_name
} else {
format!("{}.{}", obj_name, prop_name)
}
} else {
node_text(prop, source)
}
} else {
String::new()
}
}
"call_expression" => {
if let Some(func) = find_child_by_field(node, "function") {
get_call_name(func, source)
} else {
String::new()
}
}
_ => String::new(),
}
}
fn extract_function_decl(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
symbols: &mut Vec<SymbolEntry>,
) {
let name = match find_child_by_field(node, "name") {
Some(n) => node_text(n, source),
None => return,
};
let line = node_line_range(node);
let _sig = build_function_signature(node, source, &name);
let is_exported = node
.parent()
.map(|p| p.kind() == "export_statement")
.unwrap_or(false);
let visibility = if is_exported {
"public".to_string()
} else {
"private".to_string()
};
let kind = if parent_ctx.is_some() {
"method"
} else {
"function"
};
let full_name = if let Some(parent) = parent_ctx {
format!("{parent}.{name}")
} else {
name
};
let tokens = find_child_by_field(node, "body")
.and_then(|body| filter_js_tokens(extract_tokens(body, source)));
push_symbol(
symbols,
file_path,
full_name,
kind,
line,
parent_ctx,
tokens,
None,
Some(visibility),
);
}
#[allow(clippy::too_many_arguments)]
fn extract_class(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
symbols: &mut Vec<SymbolEntry>,
texts: &mut Vec<TextEntry>,
references: &mut Vec<ReferenceEntry>,
depth: usize,
) {
let name = match find_child_by_field(node, "name") {
Some(n) => node_text(n, source),
None => return,
};
let line = node_line_range(node);
let is_exported = node
.parent()
.map(|p| p.kind() == "export_statement")
.unwrap_or(false);
let visibility = if is_exported {
"public".to_string()
} else {
"private".to_string()
};
let _sig = build_class_signature(node, source, &name);
let full_name = if let Some(parent) = parent_ctx {
format!("{parent}.{name}")
} else {
name.clone()
};
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"class_heritage" => {
let mut heritage_cursor = child.walk();
for heritage_child in child.children(&mut heritage_cursor) {
if heritage_child.kind() == "extends_clause" {
if let Some(superclass) = heritage_child.child(1).or_else(|| {
let mut c = heritage_child.walk();
heritage_child
.children(&mut c)
.find(|n| n.kind() == "identifier")
}) {
let super_name = node_text(superclass, source);
if !super_name.is_empty() && !is_js_builtin_call(&super_name) {
references.push(ReferenceEntry {
file: file_path.to_string(),
name: super_name,
kind: "type_annotation".to_string(),
line: node_line_range(heritage_child),
caller: Some(full_name.clone()),
project: String::new(),
});
}
}
} else if matches!(heritage_child.kind(), "identifier" | "member_expression") {
let super_name = node_text(heritage_child, source);
if !super_name.is_empty() && !is_js_builtin_call(&super_name) {
references.push(ReferenceEntry {
file: file_path.to_string(),
name: super_name,
kind: "type_annotation".to_string(),
line: node_line_range(heritage_child),
caller: Some(full_name.clone()),
project: String::new(),
});
}
}
}
}
"extends_clause" => {
if let Some(superclass) = child.child(1).or_else(|| {
let mut c = child.walk();
child.children(&mut c).find(|n| n.kind() == "identifier")
}) {
let super_name = node_text(superclass, source);
if !super_name.is_empty() && !is_js_builtin_call(&super_name) {
references.push(ReferenceEntry {
file: file_path.to_string(),
name: super_name,
kind: "type_annotation".to_string(),
line: node_line_range(child),
caller: Some(full_name.clone()),
project: String::new(),
});
}
}
}
_ => {}
}
}
let tokens = find_child_by_field(node, "body")
.and_then(|body| filter_js_tokens(extract_tokens(body, source)));
push_symbol(
symbols,
file_path,
full_name.clone(),
"class",
line,
parent_ctx,
tokens,
None,
Some(visibility),
);
if let Some(body) = find_child_by_field(node, "body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_node(
child,
source,
file_path,
Some(&full_name),
symbols,
texts,
references,
depth + 1,
);
}
}
}
fn extract_method(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
symbols: &mut Vec<SymbolEntry>,
) {
let name = match find_child_by_field(node, "name") {
Some(n) => node_text(n, source),
None => return,
};
let line = node_line_range(node);
let mut is_static = false;
let mut is_getter = false;
let mut is_setter = false;
let mut is_async = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"static" => is_static = true,
"get" => is_getter = true,
"set" => is_setter = true,
"async" => is_async = true,
_ => {}
}
}
let kind = if is_getter || is_setter {
"property"
} else {
"method"
};
let params = find_child_by_field(node, "parameters")
.map(|n| node_text(n, source))
.unwrap_or_else(|| "()".to_string());
let mut sig_parts = Vec::new();
if is_async {
sig_parts.push("async");
}
if is_static {
sig_parts.push("static");
}
if is_getter {
sig_parts.push("get");
}
if is_setter {
sig_parts.push("set");
}
let prefix = if sig_parts.is_empty() {
String::new()
} else {
format!("{} ", sig_parts.join(" "))
};
let _sig = format!("{prefix}{name}{params}");
let visibility = if name.starts_with('#') {
"private".to_string()
} else if name.starts_with('_') {
"internal".to_string()
} else {
"public".to_string()
};
let full_name = if let Some(parent) = parent_ctx {
format!("{parent}.{name}")
} else {
name
};
let tokens = find_child_by_field(node, "body")
.and_then(|body| filter_js_tokens(extract_tokens(body, source)));
push_symbol(
symbols,
file_path,
full_name,
kind,
line,
parent_ctx,
tokens,
None,
Some(visibility),
);
}
fn extract_variable_decl(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
symbols: &mut Vec<SymbolEntry>,
) {
let line = node_line_range(node);
let is_exported = node
.parent()
.map(|p| p.kind() == "export_statement")
.unwrap_or(false);
let visibility = if is_exported {
"public".to_string()
} else {
"private".to_string()
};
let is_const = node.kind() == "lexical_declaration" && {
node.child(0)
.map(|c| node_text(c, source) == "const")
.unwrap_or(false)
};
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "variable_declarator" {
let name_node = find_child_by_field(child, "name");
let value_node = find_child_by_field(child, "value");
if let Some(n) = name_node {
if n.kind() != "identifier" {
continue;
}
let name = node_text(n, source);
let is_func = value_node
.map(|v| {
matches!(
v.kind(),
"arrow_function"
| "function"
| "function_expression"
| "generator_function"
)
})
.unwrap_or(false);
let kind = if is_func {
"function"
} else if is_const
&& name.chars().all(|c| c.is_uppercase() || c == '_')
&& name.len() > 1
{
"constant"
} else {
"variable"
};
let full_name = if let Some(parent) = parent_ctx {
format!("{parent}.{name}")
} else {
name
};
let tokens = value_node.and_then(|v| filter_js_tokens(extract_tokens(v, source)));
push_symbol(
symbols,
file_path,
full_name,
kind,
line,
parent_ctx,
tokens,
None,
Some(visibility.clone()),
);
}
}
}
}
fn extract_import(
node: Node,
source: &[u8],
file_path: &str,
symbols: &mut Vec<SymbolEntry>,
references: &mut Vec<ReferenceEntry>,
) {
let line = node_line_range(node);
let source_module = find_child_by_field(node, "source")
.map(|n| {
let raw = node_text(n, source);
strip_string_quotes(&raw)
})
.unwrap_or_default();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "import_clause" {
let mut clause_cursor = child.walk();
for clause_child in child.children(&mut clause_cursor) {
match clause_child.kind() {
"identifier" => {
let name = node_text(clause_child, source);
let full_name = source_module.clone();
push_symbol(
symbols,
file_path,
full_name.clone(),
"import",
line,
None,
None,
Some(name),
Some("private".to_string()),
);
references.push(ReferenceEntry {
file: file_path.to_string(),
name: full_name,
kind: "import".to_string(),
line,
caller: None,
project: String::new(),
});
}
"named_imports" => {
let mut named_cursor = clause_child.walk();
for spec in clause_child.children(&mut named_cursor) {
if spec.kind() == "import_specifier" {
let imported_name =
find_child_by_field(spec, "name").map(|n| node_text(n, source));
let alias = find_child_by_field(spec, "alias")
.map(|n| node_text(n, source));
if let Some(imp_name) = imported_name {
let full_name = format!("{source_module}.{imp_name}");
push_symbol(
symbols,
file_path,
full_name.clone(),
"import",
line,
None,
None,
alias,
Some("private".to_string()),
);
references.push(ReferenceEntry {
file: file_path.to_string(),
name: full_name,
kind: "import".to_string(),
line,
caller: None,
project: String::new(),
});
}
}
}
}
"namespace_import" => {
let alias = find_child_by_field(clause_child, "alias")
.or_else(|| {
let mut c = clause_child.walk();
clause_child
.children(&mut c)
.find(|n| n.kind() == "identifier")
})
.map(|n| node_text(n, source));
let full_name = format!("{source_module}.*");
push_symbol(
symbols,
file_path,
full_name.clone(),
"import",
line,
None,
None,
alias,
Some("private".to_string()),
);
references.push(ReferenceEntry {
file: file_path.to_string(),
name: full_name,
kind: "import".to_string(),
line,
caller: None,
project: String::new(),
});
}
_ => {}
}
}
}
}
}
fn extract_js_comment(
node: Node,
source: &[u8],
file_path: &str,
parent_ctx: Option<&str>,
texts: &mut Vec<TextEntry>,
) {
let raw = node_text(node, source);
let line = node_line_range(node);
let (kind, text) = if raw.starts_with("/**") {
let cleaned = strip_block_comment(&raw);
("docstring", cleaned)
} else if raw.starts_with("/*") {
let cleaned = strip_block_comment(&raw);
("comment", cleaned)
} else if raw.starts_with("//") {
let cleaned = raw.strip_prefix("//").unwrap_or(&raw).trim().to_string();
("comment", cleaned)
} else {
("comment", raw)
};
if is_trivial_text(&text) {
return;
}
texts.push(TextEntry {
file: file_path.to_string(),
kind: kind.to_string(),
line,
text,
parent: parent_ctx.map(String::from),
project: String::new(),
});
}
fn build_function_signature(node: Node, source: &[u8], name: &str) -> String {
let params = find_child_by_field(node, "parameters")
.map(|n| node_text(n, source))
.unwrap_or_else(|| "()".to_string());
let is_async = node.child(0).map(|c| c.kind() == "async").unwrap_or(false);
let is_generator = node.kind() == "generator_function_declaration";
let prefix = match (is_async, is_generator) {
(true, true) => "async function*",
(true, false) => "async function",
(false, true) => "function*",
(false, false) => "function",
};
format!("{prefix} {name}{params}")
}
fn build_class_signature(node: Node, source: &[u8], name: &str) -> String {
let extends = find_child_by_field(node, "heritage")
.or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor)
.find(|c| c.kind() == "class_heritage")
})
.map(|n| {
let text = node_text(n, source);
format!(" {text}")
})
.unwrap_or_default();
format!("class {name}{extends}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::treesitter::parse_file;
fn find_sym<'a>(symbols: &'a [SymbolEntry], name: &str) -> &'a SymbolEntry {
symbols
.iter()
.find(|s| s.name == name)
.unwrap_or_else(|| panic!("symbol not found: {name}"))
}
#[test]
fn test_js_functions() {
let source = b"function hello(name) {
return `Hello, ${name}!`;
}
async function fetchData() {
return await fetch('/api');
}
function* generator() {
yield 1;
}";
let (symbols, _texts, _refs) = parse_file(source, "javascript", "test.js").unwrap();
assert_eq!(symbols.len(), 3);
let hello = find_sym(&symbols, "hello");
assert_eq!(hello.kind, "function");
assert_eq!(hello.visibility.as_deref(), Some("private"));
let fetch = find_sym(&symbols, "fetchData");
assert_eq!(fetch.kind, "function");
let generator = find_sym(&symbols, "generator");
assert_eq!(generator.kind, "function");
}
#[test]
fn test_js_classes() {
let source = b"export class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, ${this.name}`;
}
static create() {
return new Person('default');
}
get fullName() {
return this.name;
}
}";
let (symbols, _texts, _refs) = parse_file(source, "javascript", "test.js").unwrap();
let person = find_sym(&symbols, "Person");
assert_eq!(person.kind, "class");
assert_eq!(person.visibility.as_deref(), Some("public"));
let greet = find_sym(&symbols, "Person.greet");
assert_eq!(greet.kind, "method");
assert_eq!(greet.parent.as_deref(), Some("Person"));
let create = find_sym(&symbols, "Person.create");
assert_eq!(create.kind, "method");
let getter = find_sym(&symbols, "Person.fullName");
assert_eq!(getter.kind, "property");
}
#[test]
fn test_js_variables() {
let source = b"const MAX_SIZE = 100;
let debug = true;
var legacy = 'old';
export const API_KEY = 'secret';
const add = (a, b) => a + b;
const asyncFn = async (x) => x * 2;";
let (symbols, _texts, _refs) = parse_file(source, "javascript", "test.js").unwrap();
let max = find_sym(&symbols, "MAX_SIZE");
assert_eq!(max.kind, "constant");
let debug = find_sym(&symbols, "debug");
assert_eq!(debug.kind, "variable");
let api = find_sym(&symbols, "API_KEY");
assert_eq!(api.visibility.as_deref(), Some("public"));
let add = find_sym(&symbols, "add");
assert_eq!(add.kind, "function");
let async_fn = find_sym(&symbols, "asyncFn");
assert_eq!(async_fn.kind, "function");
}
#[test]
fn test_js_imports() {
let source = b"import React from 'react';
import { useState, useEffect } from 'react';
import * as Utils from './utils';
import { render as renderDOM } from 'react-dom';";
let (symbols, _texts, _refs) = parse_file(source, "javascript", "test.js").unwrap();
let react = symbols.iter().find(|s| s.name == "react").unwrap();
assert_eq!(react.kind, "import");
assert_eq!(react.alias.as_deref(), Some("React"));
let use_state = symbols.iter().find(|s| s.name == "react.useState").unwrap();
assert_eq!(use_state.kind, "import");
let utils = symbols.iter().find(|s| s.name == "./utils.*").unwrap();
assert_eq!(utils.alias.as_deref(), Some("Utils"));
let render = symbols
.iter()
.find(|s| s.name == "react-dom.render")
.unwrap();
assert_eq!(render.alias.as_deref(), Some("renderDOM"));
}
#[test]
fn test_js_visibility() {
let source = b"class Foo {
publicMethod() {}
_internalMethod() {}
#privateMethod() {}
}";
let (symbols, _texts, _refs) = parse_file(source, "javascript", "test.js").unwrap();
let public = symbols
.iter()
.find(|s| s.name == "Foo.publicMethod")
.unwrap();
assert_eq!(public.visibility.as_deref(), Some("public"));
let internal = symbols
.iter()
.find(|s| s.name == "Foo._internalMethod")
.unwrap();
assert_eq!(internal.visibility.as_deref(), Some("internal"));
let private = symbols
.iter()
.find(|s| s.name == "Foo.#privateMethod")
.unwrap();
assert_eq!(private.visibility.as_deref(), Some("private"));
}
#[test]
fn test_js_comments() {
let source = b"/**
* JSDoc comment
*/
function documented() {}
// Single line comment
function helper() {}
/* Block comment */";
let (_symbols, texts, _refs) = parse_file(source, "javascript", "test.js").unwrap();
assert!(texts.iter().any(|t| t.kind == "docstring"));
assert!(texts.iter().any(|t| t.kind == "comment"));
}
#[test]
fn test_js_call_references() {
let source = b"function foo() {}
function bar() {
foo();
myService.doSomething();
}";
let (_symbols, _texts, refs) = parse_file(source, "javascript", "test.js").unwrap();
let calls: Vec<_> = refs.iter().filter(|r| r.kind == "call").collect();
assert!(calls.iter().any(|r| r.name == "foo"));
assert!(calls.iter().any(|r| r.name == "myService.doSomething"));
}
#[test]
fn test_js_import_references() {
let source = b"import React from 'react';
import { useState, useEffect } from 'react';
import * as Utils from './utils';";
let (_symbols, _texts, refs) = parse_file(source, "javascript", "test.js").unwrap();
let imports: Vec<_> = refs.iter().filter(|r| r.kind == "import").collect();
assert!(!imports.is_empty());
assert!(imports.iter().any(|r| r.name == "react"));
}
#[test]
fn test_js_instantiation_references() {
let source = b"class MyClass {}
const obj = new MyClass();
const service = new MyService();";
let (_symbols, _texts, refs) = parse_file(source, "javascript", "test.js").unwrap();
let instantiations: Vec<_> = refs.iter().filter(|r| r.kind == "instantiation").collect();
assert!(instantiations.iter().any(|r| r.name == "MyClass"));
assert!(instantiations.iter().any(|r| r.name == "MyService"));
}
#[test]
fn test_js_class_heritage_references() {
let source = b"class Animal {}
class Dog extends Animal {
bark() {}
}";
let (_symbols, _texts, refs) = parse_file(source, "javascript", "test.js").unwrap();
let type_refs: Vec<_> = refs
.iter()
.filter(|r| r.kind == "type_annotation")
.collect();
assert!(type_refs.iter().any(|r| r.name == "Animal"));
}
}