use sqry_core::graph::local_scopes::{self, ScopeId, ScopeKindTrait, ScopeTree};
use sqry_core::graph::unified::build::helper::GraphBuildHelper;
use sqry_core::graph::unified::node::NodeId;
use sqry_core::graph::{GraphResult, Span};
use tree_sitter::Node;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum ScopeKind {
Function,
ArrowFunction,
Method,
Block,
IfBranch,
ForLoop,
ForInLoop,
ForOfLoop,
WhileLoop,
DoWhileLoop,
TryBlock,
CatchBlock,
FinallyBlock,
SwitchBlock,
SwitchCase,
}
impl ScopeKindTrait for ScopeKind {
fn is_class_scope(&self) -> bool {
false }
fn is_overlap_boundary(&self) -> bool {
false }
fn allows_nested_shadowing(&self) -> bool {
true }
}
pub(crate) type TypeScriptScopeTree = ScopeTree<ScopeKind>;
pub(crate) fn build(root: Node, content: &[u8]) -> GraphResult<TypeScriptScopeTree> {
let content_len = content.len();
let mut tree = TypeScriptScopeTree::new(content_len);
let mut guard = local_scopes::load_recursion_guard();
build_scopes_recursive(&mut tree, root, content, None, &mut guard)?;
tree.rebuild_index();
bind_declarations_recursive(&mut tree, root, content, &mut guard)?;
tree.rebuild_index();
Ok(tree)
}
#[allow(clippy::too_many_lines)] fn build_scopes_recursive(
tree: &mut TypeScriptScopeTree,
node: Node,
content: &[u8],
current_scope: Option<ScopeId>,
guard: &mut sqry_core::query::security::RecursionGuard,
) -> GraphResult<()> {
guard
.enter()
.map_err(|e| local_scopes::recursion_error_to_graph_error(&e, node))?;
match node.kind() {
"function_declaration" | "function_expression" | "generator_function_declaration" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::Function,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"arrow_function" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::ArrowFunction,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"method_definition" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::Method,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"for_statement" => {
if node.child_by_field_name("body").is_some()
&& let Some(scope_id) = tree.add_scope(
ScopeKind::ForLoop,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, node, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"for_in_statement" => {
if node.child_by_field_name("body").is_some()
&& let Some(scope_id) = tree.add_scope(
ScopeKind::ForInLoop,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, node, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"for_of_statement" => {
if node.child_by_field_name("body").is_some()
&& let Some(scope_id) = tree.add_scope(
ScopeKind::ForOfLoop,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, node, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"while_statement" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::WhileLoop,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"do_statement" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::DoWhileLoop,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"if_statement" => {
if let Some(consequence) = node.child_by_field_name("consequence")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::IfBranch,
consequence.start_byte(),
consequence.end_byte(),
current_scope,
)
{
recurse_children(tree, consequence, content, Some(scope_id), guard)?;
}
if let Some(alternative) = node.child_by_field_name("alternative")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::IfBranch,
alternative.start_byte(),
alternative.end_byte(),
current_scope,
)
{
recurse_children(tree, alternative, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"switch_statement" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::SwitchBlock,
body.start_byte(),
body.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"switch_case" | "switch_default" => {
if let Some(scope_id) = tree.add_scope(
ScopeKind::SwitchCase,
node.start_byte(),
node.end_byte(),
current_scope,
) {
recurse_children(tree, node, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"try_statement" => {
if let Some(body) = node.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::TryBlock,
body.start_byte(),
body.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
if let Some(handler) = node.child_by_field_name("handler")
&& let Some(body) = handler.child_by_field_name("body")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::CatchBlock,
handler.start_byte(),
handler.end_byte(),
current_scope,
)
{
recurse_children(tree, body, content, Some(scope_id), guard)?;
}
if let Some(finalizer) = node.child_by_field_name("finalizer")
&& let Some(scope_id) = tree.add_scope(
ScopeKind::FinallyBlock,
finalizer.start_byte(),
finalizer.end_byte(),
current_scope,
)
{
recurse_children(tree, finalizer, content, Some(scope_id), guard)?;
}
guard.exit();
return Ok(());
}
"statement_block" => {
let parent_kind = node.parent().map(|p| p.kind());
let already_scoped = matches!(
parent_kind,
Some(
"function_declaration"
| "function_expression"
| "generator_function_declaration"
| "arrow_function"
| "method_definition"
| "for_statement"
| "for_in_statement"
| "for_of_statement"
| "while_statement"
| "do_statement"
| "if_statement"
| "try_statement"
| "catch_clause"
| "finally_clause"
| "switch_statement"
)
);
if !already_scoped
&& let Some(scope_id) = tree.add_scope(
ScopeKind::Block,
node.start_byte(),
node.end_byte(),
current_scope,
)
{
recurse_children(tree, node, content, Some(scope_id), guard)?;
guard.exit();
return Ok(());
}
}
_ => {}
}
recurse_children(tree, node, content, current_scope, guard)?;
guard.exit();
Ok(())
}
fn recurse_children(
tree: &mut TypeScriptScopeTree,
node: Node,
content: &[u8],
scope: Option<ScopeId>,
guard: &mut sqry_core::query::security::RecursionGuard,
) -> GraphResult<()> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
build_scopes_recursive(tree, child, content, scope, guard)?;
}
Ok(())
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)] fn bind_declarations_recursive(
tree: &mut TypeScriptScopeTree,
node: Node,
content: &[u8],
guard: &mut sqry_core::query::security::RecursionGuard,
) -> GraphResult<()> {
guard
.enter()
.map_err(|e| local_scopes::recursion_error_to_graph_error(&e, node))?;
match node.kind() {
"lexical_declaration" | "variable_declaration" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte()) {
bind_variable_declarators(tree, scope_id, node, content);
}
}
"function_declaration" | "function_expression" | "generator_function_declaration" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte())
&& let Some(params) = node.child_by_field_name("parameters")
{
bind_parameters(tree, scope_id, params, content);
}
}
"arrow_function" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte()) {
if let Some(params) = node.child_by_field_name("parameters") {
if params.kind() == "formal_parameters" {
bind_parameters(tree, scope_id, params, content);
} else {
bind_identifier_as_param(tree, scope_id, params, content);
}
}
if let Some(param) = node.child_by_field_name("parameter") {
bind_identifier_as_param(tree, scope_id, param, content);
}
}
}
"method_definition" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte())
&& let Some(params) = node.child_by_field_name("parameters")
{
bind_parameters(tree, scope_id, params, content);
}
}
"for_in_statement" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte())
&& let Some(left) = node.child_by_field_name("left")
{
bind_for_loop_variable(tree, scope_id, left, node, content);
}
}
"for_of_statement" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte())
&& let Some(left) = node.child_by_field_name("left")
{
bind_for_loop_variable(tree, scope_id, left, node, content);
}
}
"catch_clause" => {
if let Some(scope_id) = tree.innermost_scope_at(node.start_byte()) {
bind_catch_parameter(tree, scope_id, node, content);
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
bind_declarations_recursive(tree, child, content, guard)?;
}
guard.exit();
Ok(())
}
fn bind_variable_declarators(
tree: &mut TypeScriptScopeTree,
scope_id: ScopeId,
decl: Node,
content: &[u8],
) {
let mut cursor = decl.walk();
for child in decl.children(&mut cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
{
let initializer_start = child.child_by_field_name("value").map(|v| v.start_byte());
bind_pattern(
tree,
scope_id,
name_node,
child.end_byte(),
initializer_start,
content,
);
}
}
}
fn bind_parameters(
tree: &mut TypeScriptScopeTree,
scope_id: ScopeId,
params: Node,
content: &[u8],
) {
let mut cursor = params.walk();
for child in params.children(&mut cursor) {
match child.kind() {
"required_parameter" | "optional_parameter" => {
if let Some(pattern) = child.child_by_field_name("pattern") {
bind_pattern(tree, scope_id, pattern, child.end_byte(), None, content);
} else if let Some(name_node) = child.child_by_field_name("name") {
bind_pattern(tree, scope_id, name_node, child.end_byte(), None, content);
} else {
bind_identifier_as_param(tree, scope_id, child, content);
}
}
_ => {}
}
}
}
fn bind_identifier_as_param(
tree: &mut TypeScriptScopeTree,
scope_id: ScopeId,
node: Node,
content: &[u8],
) {
if node.kind() == "identifier" {
if let Ok(name) = node.utf8_text(content)
&& !name.is_empty()
{
tree.add_binding(
scope_id,
name,
node.start_byte(),
node.end_byte(),
node.end_byte(),
None,
);
}
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
if let Ok(name) = child.utf8_text(content)
&& !name.is_empty()
{
tree.add_binding(
scope_id,
name,
child.start_byte(),
child.end_byte(),
child.end_byte(),
None,
);
}
return;
}
}
}
#[allow(clippy::too_many_lines)] fn bind_pattern(
tree: &mut TypeScriptScopeTree,
scope_id: ScopeId,
node: Node,
declarator_end: usize,
initializer_start: Option<usize>,
content: &[u8],
) {
match node.kind() {
"identifier" => {
if let Ok(name) = node.utf8_text(content)
&& !name.is_empty()
{
tree.add_binding(
scope_id,
name,
node.start_byte(),
node.end_byte(),
declarator_end,
initializer_start,
);
}
}
"array_pattern" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "array_pattern" | "object_pattern" | "assignment_pattern" => {
bind_pattern(
tree,
scope_id,
child,
declarator_end,
initializer_start,
content,
);
}
"rest_element" => {
let mut inner = child.walk();
for item in child.children(&mut inner) {
if item.kind() == "identifier"
|| item.kind() == "array_pattern"
|| item.kind() == "object_pattern"
{
bind_pattern(
tree,
scope_id,
item,
declarator_end,
initializer_start,
content,
);
break;
}
}
}
_ => {}
}
}
}
"object_pattern" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"shorthand_property_identifier_pattern" | "shorthand_property_identifier" => {
if let Ok(name) = child.utf8_text(content)
&& !name.is_empty()
{
tree.add_binding(
scope_id,
name,
child.start_byte(),
child.end_byte(),
declarator_end,
initializer_start,
);
}
}
"pair_pattern" => {
if let Some(value) = child.child_by_field_name("value") {
bind_pattern(
tree,
scope_id,
value,
declarator_end,
initializer_start,
content,
);
}
}
"rest_element" => {
let mut inner = child.walk();
for item in child.children(&mut inner) {
if item.kind() == "identifier"
|| item.kind() == "object_pattern"
|| item.kind() == "array_pattern"
{
bind_pattern(
tree,
scope_id,
item,
declarator_end,
initializer_start,
content,
);
break;
}
}
}
_ => {}
}
}
}
"assignment_pattern" => {
if let Some(left) = node.child_by_field_name("left") {
bind_pattern(
tree,
scope_id,
left,
declarator_end,
initializer_start,
content,
);
}
}
"rest_pattern" | "rest_element" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "array_pattern" | "object_pattern" => {
bind_pattern(
tree,
scope_id,
child,
declarator_end,
initializer_start,
content,
);
break;
}
_ => {}
}
}
}
_ => {}
}
}
fn bind_for_loop_variable(
tree: &mut TypeScriptScopeTree,
scope_id: ScopeId,
left: Node,
_for_node: Node,
content: &[u8],
) {
match left.kind() {
"identifier" => {
if let Ok(name) = left.utf8_text(content)
&& !name.is_empty()
{
tree.add_binding(
scope_id,
name,
left.start_byte(),
left.end_byte(),
left.end_byte(),
None,
);
}
}
"lexical_declaration" | "variable_declaration" => {
let mut cursor = left.walk();
for child in left.children(&mut cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
{
bind_pattern(tree, scope_id, name_node, left.end_byte(), None, content);
}
}
}
_ => {}
}
}
fn bind_catch_parameter(
tree: &mut TypeScriptScopeTree,
scope_id: ScopeId,
catch_node: Node,
content: &[u8],
) {
if let Some(param) = catch_node.child_by_field_name("parameter") {
bind_pattern(tree, scope_id, param, param.end_byte(), None, content);
}
}
pub(crate) fn handle_identifier_for_reference(
node: Node,
content: &[u8],
scope_tree: &mut TypeScriptScopeTree,
helper: &mut GraphBuildHelper,
) {
let identifier = node.utf8_text(content).unwrap_or("");
if identifier.is_empty() || identifier == "_" {
return;
}
if is_declaration_context(node) {
return;
}
if is_type_or_call_context(node) {
return;
}
if is_member_access(node) {
return;
}
if !is_inside_function(node) {
return;
}
let usage_byte = node.start_byte();
match scope_tree.resolve_identifier(usage_byte, identifier) {
local_scopes::ResolutionOutcome::Local(binding) => {
let target_id = if let Some(node_id) = binding.node_id {
node_id
} else {
let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
let qualified_var = format!("{identifier}@{}", binding.decl_start_byte);
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(identifier, binding.decl_start_byte, var_id);
var_id
};
add_reference_edge(node, identifier, target_id, helper);
}
local_scopes::ResolutionOutcome::Member { .. }
| local_scopes::ResolutionOutcome::Ambiguous
| local_scopes::ResolutionOutcome::NoMatch => {}
}
}
fn add_reference_edge(
usage_node: Node,
identifier: &str,
target_id: NodeId,
helper: &mut GraphBuildHelper,
) {
let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
let usage_id = helper.add_node(
&format!("{identifier}@{}", usage_node.start_byte()),
Some(usage_span),
sqry_core::graph::unified::node::NodeKind::Variable,
);
helper.add_reference_edge(usage_id, target_id);
}
#[allow(clippy::match_same_arms)] fn is_declaration_context(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
match parent.kind() {
"variable_declarator" => parent
.child_by_field_name("name")
.is_some_and(|n| n.id() == node.id()),
"function_declaration" | "function_expression" | "generator_function_declaration" => parent
.child_by_field_name("name")
.is_some_and(|n| n.id() == node.id()),
"required_parameter" | "optional_parameter" | "rest_parameter" => true,
"shorthand_property_identifier_pattern" | "shorthand_property_identifier" => true,
"pair_pattern" => parent
.child_by_field_name("key")
.is_some_and(|n| n.id() == node.id()),
"assignment_pattern" => parent
.child_by_field_name("left")
.is_some_and(|n| n.id() == node.id()),
"pair" => parent
.child_by_field_name("key")
.is_some_and(|n| n.id() == node.id()),
"for_in_statement" | "for_of_statement" => parent
.child_by_field_name("left")
.is_some_and(|n| n.id() == node.id()),
"labeled_statement" => parent
.child_by_field_name("label")
.is_some_and(|n| n.id() == node.id()),
"class_declaration" | "class" | "interface_declaration" => parent
.child_by_field_name("name")
.is_some_and(|n| n.id() == node.id()),
"type_alias_declaration" => parent
.child_by_field_name("name")
.is_some_and(|n| n.id() == node.id()),
"enum_declaration" => parent
.child_by_field_name("name")
.is_some_and(|n| n.id() == node.id()),
"method_definition" => parent
.child_by_field_name("name")
.is_some_and(|n| n.id() == node.id()),
"catch_clause" => parent
.child_by_field_name("parameter")
.is_some_and(|n| n.id() == node.id()),
"import_specifier" | "import_clause" | "namespace_import" => true,
"export_specifier" => true,
_ => false,
}
}
fn is_type_or_call_context(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
match parent.kind() {
"type_annotation"
| "type_identifier"
| "predefined_type"
| "generic_type"
| "type_arguments"
| "type_parameter"
| "constraint"
| "extends_clause"
| "implements_clause"
| "extends_type_clause" => true,
"call_expression" => parent
.child_by_field_name("function")
.is_some_and(|n| n.id() == node.id()),
"new_expression" => parent
.child_by_field_name("constructor")
.is_some_and(|n| n.id() == node.id()),
_ => false,
}
}
fn is_member_access(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
match parent.kind() {
"member_expression" => parent
.child_by_field_name("property")
.is_some_and(|n| n.id() == node.id()),
"property_identifier" => true,
_ => false,
}
}
fn is_inside_function(node: Node) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
match parent.kind() {
"function_declaration"
| "function_expression"
| "generator_function_declaration"
| "arrow_function"
| "method_definition" => {
return true;
}
"program" => return false,
_ => {}
}
current = parent.parent();
}
false
}