use sqry_core::graph::GraphBuilder;
use sqry_core::graph::unified::build::{StagingGraph, StagingOp};
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_lang_javascript::JavaScriptGraphBuilder;
use std::collections::HashMap;
use std::path::Path;
fn build_graph(source: &str) -> StagingGraph {
let builder = JavaScriptGraphBuilder::default();
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_javascript::LANGUAGE.into())
.expect("Failed to load JavaScript grammar");
let tree = parser.parse(source, None).expect("Failed to parse");
let mut staging = StagingGraph::new();
let file_path = Path::new("test.js");
builder
.build_graph(&tree, source.as_bytes(), file_path, &mut staging)
.expect("Failed to build graph");
staging
}
fn build_string_lookup(staging: &StagingGraph) -> HashMap<u32, String> {
staging
.operations()
.iter()
.filter_map(|op| {
if let StagingOp::InternString { local_id, value } = op {
Some((local_id.index(), value.clone()))
} else {
None
}
})
.collect()
}
fn build_node_name_lookup(staging: &StagingGraph) -> HashMap<u32, String> {
let strings = build_string_lookup(staging);
staging
.operations()
.iter()
.filter_map(|op| {
if let StagingOp::AddNode { entry, expected_id } = op {
let expected_id = expected_id.as_ref()?;
let node_idx = expected_id.index();
let name_idx = entry.qualified_name.unwrap_or(entry.name).index();
let name = strings
.get(&name_idx)
.cloned()
.unwrap_or_else(|| format!("<string:{name_idx}>"));
Some((node_idx, name))
} else {
None
}
})
.collect()
}
fn collect_edges_by_kind<F>(staging: &StagingGraph, predicate: F) -> Vec<(String, String)>
where
F: Fn(&EdgeKind) -> bool,
{
let node_names = build_node_name_lookup(staging);
let mut edges = Vec::new();
for op in staging.operations() {
if let StagingOp::AddEdge {
source,
target,
kind,
..
} = op
&& predicate(kind)
{
let from_name = node_names
.get(&source.index())
.cloned()
.unwrap_or_else(|| format!("<unknown:{}>", source.index()));
let to_name = node_names
.get(&target.index())
.cloned()
.unwrap_or_else(|| format!("<unknown:{}>", target.index()));
edges.push((from_name, to_name));
}
}
edges
}
fn has_typeof_edge(
staging: &StagingGraph,
source_name: &str,
target_type: &str,
context: TypeOfContext,
) -> bool {
let edges = collect_edges_by_kind(staging, |kind| {
matches!(
kind,
EdgeKind::TypeOf {
context: Some(ctx),
..
} if *ctx == context
)
});
edges
.iter()
.any(|(src, tgt)| src == source_name && tgt == target_type)
}
fn has_reference_edge(staging: &StagingGraph, source_name: &str, target_type: &str) -> bool {
let edges = collect_edges_by_kind(staging, |kind| matches!(kind, EdgeKind::References));
edges
.iter()
.any(|(src, tgt)| src == source_name && tgt == target_type)
}
#[test]
fn test_simple_parameter_types() {
let source = r"
/**
* @param {string} name
* @param {number} age
* @param {boolean} active
*/
function createUser(name, age, active) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"createUser",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"createUser",
"number",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"createUser",
"boolean",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "createUser", "string"));
assert!(has_reference_edge(&graph, "createUser", "number"));
assert!(has_reference_edge(&graph, "createUser", "boolean"));
}
#[test]
fn test_complex_parameter_types() {
let source = r"
/**
* @param {Array<User>} users
* @param {Map<string, number>} scores
*/
function processData(users, scores) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"processData",
"Array<User>",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"processData",
"Map<string, number>",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "processData", "Array"));
assert!(has_reference_edge(&graph, "processData", "User"));
assert!(has_reference_edge(&graph, "processData", "Map"));
assert!(has_reference_edge(&graph, "processData", "string"));
assert!(has_reference_edge(&graph, "processData", "number"));
}
#[test]
fn test_optional_parameters() {
let source = r"
/**
* @param {string} name
* @param {string} [optionalTag]
*/
function process(name, optionalTag) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"process",
"string",
TypeOfContext::Parameter
));
}
#[test]
fn test_rest_parameters() {
let source = r"
/**
* @param {string} name
* @param {...number} scores
*/
function calculate(name, ...scores) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"calculate",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"calculate",
"number",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "calculate", "string"));
assert!(has_reference_edge(&graph, "calculate", "number"));
}
#[test]
fn test_default_parameters() {
let source = r"
/**
* @param {string} name
* @param {number} [count=10]
* @param {boolean} [active=false]
*/
function create(name, count = 10, active = false) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"create",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"create",
"number",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"create",
"boolean",
TypeOfContext::Parameter
));
}
#[test]
fn test_simple_return_types() {
let source = r"
/**
* @returns {boolean}
*/
function isValid() {
return true;
}
/**
* @returns {User}
*/
function getUser() {
return currentUser;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"isValid",
"boolean",
TypeOfContext::Return
));
assert!(has_typeof_edge(
&graph,
"getUser",
"User",
TypeOfContext::Return
));
assert!(has_reference_edge(&graph, "isValid", "boolean"));
assert!(has_reference_edge(&graph, "getUser", "User"));
}
#[test]
fn test_promise_return_types() {
let source = r"
/**
* @param {string} id
* @returns {Promise<User>}
*/
async function fetchUser(id) {
return user;
}
/**
* @returns {Promise<Array<Item>>}
*/
async function fetchItems() {
return items;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"fetchUser",
"Promise<User>",
TypeOfContext::Return
));
assert!(has_typeof_edge(
&graph,
"fetchItems",
"Promise<Array<Item>>",
TypeOfContext::Return
));
assert!(has_reference_edge(&graph, "fetchUser", "Promise"));
assert!(has_reference_edge(&graph, "fetchUser", "User"));
assert!(has_reference_edge(&graph, "fetchItems", "Array"));
assert!(has_reference_edge(&graph, "fetchItems", "Item"));
}
#[test]
fn test_complex_return_types() {
let source = r"
/**
* @returns {{id: string, user: User, count: number}}
*/
function getData() {
return data;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"getData",
"{id: string, user: User, count: number}",
TypeOfContext::Return
));
assert!(has_reference_edge(&graph, "getData", "string"));
assert!(has_reference_edge(&graph, "getData", "User"));
assert!(has_reference_edge(&graph, "getData", "number"));
}
#[test]
fn test_variable_type_annotations() {
let source = r"
/**
* @type {Map<string, User>}
*/
const userCache = new Map();
/**
* @type {Array<number>}
*/
let scores = [];
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"userCache",
"Map<string, User>",
TypeOfContext::Variable
));
assert!(has_typeof_edge(
&graph,
"scores",
"Array<number>",
TypeOfContext::Variable
));
assert!(has_reference_edge(&graph, "userCache", "Map"));
assert!(has_reference_edge(&graph, "userCache", "string"));
assert!(has_reference_edge(&graph, "userCache", "User"));
assert!(has_reference_edge(&graph, "scores", "Array"));
assert!(has_reference_edge(&graph, "scores", "number"));
}
#[test]
fn test_constant_type_annotations() {
let source = r"
/**
* @type {Config}
*/
const CONFIG = loadConfig();
/**
* @type {Logger}
*/
const logger = createLogger();
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"CONFIG",
"Config",
TypeOfContext::Variable
));
assert!(has_typeof_edge(
&graph,
"logger",
"Logger",
TypeOfContext::Variable
));
assert!(has_reference_edge(&graph, "CONFIG", "Config"));
assert!(has_reference_edge(&graph, "logger", "Logger"));
}
#[test]
fn test_nested_object_type_parsing() {
let source = r"
/**
* @param {{id: string, meta: {tags: string[]}}} obj
*/
function processObject(obj) {}
/**
* @type {{user: {id: string, name: string}, count: number}}
*/
const data = loadData();
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"processObject",
"{id: string, meta: {tags: string[]}}",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"data",
"{user: {id: string, name: string}, count: number}",
TypeOfContext::Variable
));
assert!(has_reference_edge(&graph, "processObject", "string"));
assert!(has_reference_edge(&graph, "data", "string"));
assert!(has_reference_edge(&graph, "data", "number"));
}
#[test]
fn test_class_field_type_annotations() {
let source = r"
class DataService {
/**
* @type {Database}
*/
db;
/**
* @type {Logger}
*/
logger;
/**
* @type {Map<string, any>}
*/
cache = new Map();
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"DataService::db",
"Database",
TypeOfContext::Field
));
assert!(has_typeof_edge(
&graph,
"DataService::logger",
"Logger",
TypeOfContext::Field
));
assert!(has_typeof_edge(
&graph,
"DataService::cache",
"Map<string, any>",
TypeOfContext::Field
));
assert!(has_reference_edge(&graph, "DataService::db", "Database"));
assert!(has_reference_edge(&graph, "DataService::logger", "Logger"));
assert!(has_reference_edge(&graph, "DataService::cache", "Map"));
assert!(has_reference_edge(&graph, "DataService::cache", "string"));
}
#[test]
fn test_multi_param_function() {
let source = r"
/**
* @param {User} user
* @param {number} count
* @param {Array<string>} tags
* @param {boolean} force
*/
function updateUser(user, count, tags, force) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"updateUser",
"User",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"updateUser",
"number",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"updateUser",
"Array<string>",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"updateUser",
"boolean",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "updateUser", "User"));
assert!(has_reference_edge(&graph, "updateUser", "number"));
assert!(has_reference_edge(&graph, "updateUser", "Array"));
assert!(has_reference_edge(&graph, "updateUser", "string"));
assert!(has_reference_edge(&graph, "updateUser", "boolean"));
}
#[test]
fn test_class_with_multiple_fields() {
let source = r"
class Service {
/**
* @type {API}
*/
api;
counter = 0;
/**
* @type {EventEmitter}
*/
events;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"Service::api",
"API",
TypeOfContext::Field
));
assert!(has_typeof_edge(
&graph,
"Service::events",
"EventEmitter",
TypeOfContext::Field
));
assert!(has_reference_edge(&graph, "Service::api", "API"));
assert!(has_reference_edge(
&graph,
"Service::events",
"EventEmitter"
));
}
#[test]
fn test_union_types() {
let source = r"
/**
* @param {string|number|boolean} value
* @param {User|Admin|Guest} actor
*/
function process(value, actor) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"process",
"string|number|boolean",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"process",
"User|Admin|Guest",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "process", "string"));
assert!(has_reference_edge(&graph, "process", "number"));
assert!(has_reference_edge(&graph, "process", "boolean"));
assert!(has_reference_edge(&graph, "process", "User"));
assert!(has_reference_edge(&graph, "process", "Admin"));
assert!(has_reference_edge(&graph, "process", "Guest"));
}
#[test]
fn test_generic_types() {
let source = r"
/**
* @param {Array<T>} items
* @param {Map<K, V>} lookup
* @param {Promise<Result<Data>>} result
*/
function handle(items, lookup, result) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"handle",
"Array<T>",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"handle",
"Map<K, V>",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"handle",
"Promise<Result<Data>>",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "handle", "Array"));
assert!(has_reference_edge(&graph, "handle", "T"));
assert!(has_reference_edge(&graph, "handle", "Map"));
assert!(has_reference_edge(&graph, "handle", "K"));
assert!(has_reference_edge(&graph, "handle", "V"));
assert!(has_reference_edge(&graph, "handle", "Promise"));
assert!(has_reference_edge(&graph, "handle", "Result"));
assert!(has_reference_edge(&graph, "handle", "Data"));
}
#[test]
fn test_intersection_types() {
let source = r"
/**
* @param {Readable&Writable} stream
* @param {User&Admin} superuser
*/
function operate(stream, superuser) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"operate",
"Readable&Writable",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"operate",
"User&Admin",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "operate", "Readable"));
assert!(has_reference_edge(&graph, "operate", "Writable"));
assert!(has_reference_edge(&graph, "operate", "User"));
assert!(has_reference_edge(&graph, "operate", "Admin"));
}
#[test]
fn test_nested_generics() {
let source = r"
/**
* @param {Promise<Array<User>>} users
*/
function processUsers(users) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"processUsers",
"Promise<Array<User>>",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "processUsers", "Promise"));
assert!(has_reference_edge(&graph, "processUsers", "Array"));
assert!(has_reference_edge(&graph, "processUsers", "User"));
}
#[test]
fn test_no_jsdoc_comments() {
let source = r"
// Regular comment
function noJsDoc(a, b) {
return a + b;
}
/* Block comment but not JSDoc */
const x = 42;
class NoTypes {
field = 'value';
}
";
let graph = build_graph(source);
let typeof_edges =
collect_edges_by_kind(&graph, |kind| matches!(kind, EdgeKind::TypeOf { .. }));
assert!(
typeof_edges.is_empty(),
"Expected no TypeOf edges, but found: {typeof_edges:?}"
);
}
#[test]
fn test_line_comments() {
let source = r"
// @param {string} name - this is not JSDoc
function notJsDoc(name) {}
";
let graph = build_graph(source);
assert!(!has_typeof_edge(
&graph,
"notJsDoc",
"string",
TypeOfContext::Parameter
));
}
#[test]
fn test_multi_line_comments() {
let source = r"
/*
* @param {string} name - this is not JSDoc either
*/
function alsoNotJsDoc(name) {}
";
let graph = build_graph(source);
assert!(!has_typeof_edge(
&graph,
"alsoNotJsDoc",
"string",
TypeOfContext::Parameter
));
}
#[test]
fn test_distant_comments() {
let source = r"
/**
* @param {string} name
*/
// Too far away (more than 1 blank line)
function distantJsDoc(name) {}
";
let graph = build_graph(source);
assert!(!has_typeof_edge(
&graph,
"distantJsDoc",
"string",
TypeOfContext::Parameter
));
}
#[test]
fn test_full_file_integration() {
let source = r"
/**
* @type {Config}
*/
const config = loadConfig();
class UserService {
/**
* @type {Database}
*/
db;
/**
* @param {string} id
* @returns {Promise<User|null>}
*/
async getUser(id) {
return user;
}
/**
* @param {User} user
* @param {UpdateOptions} options
* @returns {Promise<boolean>}
*/
async updateUser(user, options) {
return true;
}
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"config",
"Config",
TypeOfContext::Variable
));
assert!(has_typeof_edge(
&graph,
"UserService::db",
"Database",
TypeOfContext::Field
));
assert!(has_typeof_edge(
&graph,
"UserService::getUser",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"User",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"UpdateOptions",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::getUser",
"Promise<User|null>",
TypeOfContext::Return
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"Promise<boolean>",
TypeOfContext::Return
));
assert!(has_reference_edge(&graph, "config", "Config"));
assert!(has_reference_edge(&graph, "UserService::db", "Database"));
assert!(has_reference_edge(
&graph,
"UserService::getUser",
"Promise"
));
assert!(has_reference_edge(&graph, "UserService::getUser", "User"));
assert!(has_reference_edge(
&graph,
"UserService::updateUser",
"UpdateOptions"
));
}
#[test]
fn test_qualified_types() {
let source = r"
/**
* @param {import('./models').User} user
* @param {React.Component} comp
* @param {API.Response<Data>} response
*/
function processImports(user, comp, response) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"processImports",
"import('./models').User",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"processImports",
"React::Component",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"processImports",
"API::Response<Data>",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "processImports", "User"));
assert!(has_reference_edge(&graph, "processImports", "React"));
assert!(has_reference_edge(&graph, "processImports", "Component"));
assert!(has_reference_edge(&graph, "processImports", "API"));
assert!(has_reference_edge(&graph, "processImports", "Response"));
assert!(has_reference_edge(&graph, "processImports", "Data"));
}
#[test]
fn test_edge_cases() {
let source = r"
/**
* @param {string|null|undefined} maybeValue
*/
function process(maybeValue) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"process",
"string|null|undefined",
TypeOfContext::Parameter
));
assert!(has_reference_edge(&graph, "process", "string"));
assert!(!has_reference_edge(&graph, "process", "null"));
assert!(!has_reference_edge(&graph, "process", "undefined"));
}
#[test]
fn test_top_level_vs_function_scoped_variables() {
let source = r"
/**
* @type {Config}
*/
const topLevel = getConfig();
function myFunc() {
/**
* @type {number}
*/
const functionScoped = 42;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"topLevel",
"Config",
TypeOfContext::Variable
));
assert!(!has_typeof_edge(
&graph,
"functionScoped",
"number",
TypeOfContext::Variable
));
}
#[test]
fn test_optional_parameter() {
let source = r"
/**
* @param {string} name
* @param {number} [age]
*/
function createPerson(name, age) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"createPerson",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"createPerson",
"number",
TypeOfContext::Parameter
));
}
#[test]
fn test_rest_parameter_normalization() {
let source = r"
/**
* @param {...string} args
*/
function concat(...args) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"concat",
"string",
TypeOfContext::Parameter
));
}
#[test]
fn test_dotted_parameter_names() {
let source = r"
/**
* @param {string} options.name
* @param {number} options.count
*/
function configure(options) {}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"configure",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"configure",
"number",
TypeOfContext::Parameter
));
}
#[test]
fn test_export_wrappers() {
let source = r"
/**
* @param {User} user
* @returns {boolean}
*/
export default function validateUser(user) {
return true;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"validateUser",
"User",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"validateUser",
"boolean",
TypeOfContext::Return
));
}
#[test]
fn test_class_methods_with_jsdoc() {
let source = r"
class UserService {
/**
* @param {string} id
* @returns {Promise<User>}
*/
async getUser(id) {
return user;
}
/**
* @param {User} user
* @param {UpdateOptions} options
* @returns {Promise<boolean>}
*/
async updateUser(user, options) {
return true;
}
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"UserService::getUser",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::getUser",
"Promise<User>",
TypeOfContext::Return
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"User",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"UpdateOptions",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"Promise<boolean>",
TypeOfContext::Return
));
assert!(has_reference_edge(&graph, "UserService::getUser", "string"));
assert!(has_reference_edge(
&graph,
"UserService::getUser",
"Promise"
));
assert!(has_reference_edge(&graph, "UserService::getUser", "User"));
assert!(has_reference_edge(
&graph,
"UserService::updateUser",
"UpdateOptions"
));
assert!(has_reference_edge(
&graph,
"UserService::updateUser",
"boolean"
));
}
#[test]
fn test_param_jsdoc_out_of_order() {
let source = r"
/**
* @param {number} age - second in JSDoc, but first in AST
* @param {string} name - first in JSDoc, but second in AST
*/
function greet(name, age) {
return `Hello ${name}, ${age}`;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"greet",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"greet",
"number",
TypeOfContext::Parameter
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Parameter),
..
}
)
});
assert_eq!(
typeof_edges.len(),
2,
"Should have 2 parameter TypeOf edges"
);
}
#[test]
fn test_param_jsdoc_missing_some() {
let source = r"
/**
* @param {string} name
*/
function greet(name, age, city) {
return `Hello ${name}`;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"greet",
"string",
TypeOfContext::Parameter
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Parameter),
..
}
)
});
assert_eq!(
typeof_edges.len(),
1,
"Should have only 1 parameter TypeOf edge"
);
}
#[test]
fn test_param_jsdoc_extra_tags() {
let source = r"
/**
* @param {string} name
* @param {number} age
* @param {string} city
*/
function greet(name) {
return `Hello ${name}`;
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"greet",
"string",
TypeOfContext::Parameter
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Parameter),
..
}
)
});
assert_eq!(
typeof_edges.len(),
1,
"Should have only 1 parameter TypeOf edge"
);
assert!(!has_reference_edge(&graph, "greet", "number"));
}
#[test]
fn test_method_param_jsdoc_out_of_order() {
let source = r"
class UserService {
/**
* @param {UpdateOptions} options - second in JSDoc
* @param {User} user - first in JSDoc
* @returns {boolean}
*/
updateUser(user, options) {
return true;
}
}
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"User",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::updateUser",
"UpdateOptions",
TypeOfContext::Parameter
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Parameter),
..
}
)
});
assert_eq!(
typeof_edges.len(),
2,
"Should have 2 parameter TypeOf edges"
);
}
#[test]
fn test_variable_in_if_block_no_typeof() {
let source = r"
if (true) {
/** @type {User} */
const user = getUser();
}
";
let graph = build_graph(source);
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Variable),
..
}
)
});
assert_eq!(
typeof_edges.len(),
0,
"Block-scoped variable should not get TypeOf edge"
);
}
#[test]
fn test_variable_in_for_loop_no_typeof() {
let source = r"
for (let i = 0; i < 10; i++) {
/** @type {Item} */
const item = items[i];
}
";
let graph = build_graph(source);
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Variable),
..
}
)
});
assert_eq!(
typeof_edges.len(),
0,
"Loop variable should not get TypeOf edge"
);
}
#[test]
fn test_variable_in_try_block_no_typeof() {
let source = r"
try {
/** @type {Result} */
const result = riskyOperation();
} catch (e) {
/** @type {Error} */
const error = e;
}
";
let graph = build_graph(source);
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Variable),
..
}
)
});
assert_eq!(
typeof_edges.len(),
0,
"Try/catch variables should not get TypeOf edges"
);
}
#[test]
fn test_module_level_variable_gets_typeof() {
let source = r"
/** @type {Config} */
const config = loadConfig();
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"config",
"Config",
TypeOfContext::Variable
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Variable),
..
}
)
});
assert!(
!typeof_edges.is_empty(),
"Module-level variable should get TypeOf edge"
);
}
#[test]
fn test_exported_variable_gets_typeof() {
let source = r"
/** @type {Settings} */
export const settings = {};
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"settings",
"Settings",
TypeOfContext::Variable
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Variable),
..
}
)
});
assert!(
!typeof_edges.is_empty(),
"Exported variable should get TypeOf edge"
);
}
#[test]
fn test_variable_in_while_loop_no_typeof() {
let source = r"
while (condition) {
/** @type {Item} */
const item = getNext();
}
";
let graph = build_graph(source);
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(
k,
sqry_core::graph::unified::edge::EdgeKind::TypeOf {
context: Some(TypeOfContext::Variable),
..
}
)
});
assert_eq!(
typeof_edges.len(),
0,
"While loop variable should not get TypeOf edge"
);
}
#[test]
fn test_anonymous_class_variable_assignment() {
let source = r"
const UserService = class {
/**
* @param {string} id
* @returns {User}
*/
async getUser(id) {
return await fetch(`/users/${id}`);
}
};
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"UserService::getUser",
"string",
TypeOfContext::Parameter
));
assert!(has_typeof_edge(
&graph,
"UserService::getUser",
"User",
TypeOfContext::Return
));
let typeof_edges = collect_edges_by_kind(&graph, |k| {
matches!(k, sqry_core::graph::unified::edge::EdgeKind::TypeOf { .. })
});
assert!(
typeof_edges.len() >= 2,
"Anonymous class method should get TypeOf edges"
);
}
#[test]
fn test_anonymous_class_expression() {
let source = r"
export default class {
/**
* @param {number} count
*/
increment(count) {
this.value += count;
}
};
";
let graph = build_graph(source);
let _all_edges = collect_edges_by_kind(&graph, |_| true);
}
#[test]
fn test_anonymous_class_field_jsdoc() {
let source = r"
const DataService = class {
/**
* @type {Database}
*/
db;
/**
* @type {Logger}
*/
logger;
};
";
let graph = build_graph(source);
assert!(has_typeof_edge(
&graph,
"DataService::db",
"Database",
TypeOfContext::Field
));
assert!(has_typeof_edge(
&graph,
"DataService::logger",
"Logger",
TypeOfContext::Field
));
}