use crate::engine::lang::Language;
use crate::engine::source::SourceFile;
use crate::engine::symbols::tree_sitter_language;
use serde::Serialize;
use tree_sitter::{Node, Parser, Tree};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum OccurrenceKind {
Definition,
Reference,
Shadowed,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Occurrence {
pub(crate) start_byte: usize,
pub(crate) end_byte: usize,
pub(crate) kind: OccurrenceKind,
}
#[derive(Debug)]
pub(crate) struct Binding {
pub(crate) name: String,
pub(crate) occurrences: Vec<Occurrence>,
}
#[derive(Debug)]
pub(crate) struct Conflict {
pub(crate) byte: usize,
pub(crate) reason: String,
}
pub(crate) fn supported(language: Language) -> bool {
binding_table(language).is_some()
}
fn parse_source(source: &SourceFile) -> Option<(&'static BindingTable, Tree)> {
let table = binding_table(source.detection.language)?;
let language = tree_sitter_language(source.detection.language)?;
let mut parser = Parser::new();
parser.set_language(&language).ok()?;
let tree = parser.parse(&source.text, None)?;
Some((table, tree))
}
pub(crate) fn resolve(source: &SourceFile, byte: usize) -> Option<Binding> {
resolve_with_conflicts(source, byte, None).map(|(binding, _)| binding)
}
pub(crate) fn resolve_with_conflicts(
source: &SourceFile,
byte: usize,
new_name: Option<&str>,
) -> Option<(Binding, Vec<Conflict>)> {
let (table, tree) = parse_source(source)?;
let root = tree.root_node();
let src = source.text.as_bytes();
let lookup = byte.min(source.text.len().saturating_sub(1));
let cursor = identifier_leaf(root.descendant_for_byte_range(lookup, lookup)?, table)?;
let name = cursor.utf8_text(src).ok()?.to_owned();
let mut declarations = Vec::new();
collect_declarations(root, src, table, &mut declarations);
let target_def = resolve_node(cursor, &name, &declarations, table)?;
let mut occurrences = Vec::new();
collect_occurrences(
root,
src,
table,
&name,
target_def,
&declarations,
&mut occurrences,
);
occurrences.sort_by_key(|occurrence| occurrence.start_byte);
let conflicts = new_name
.map(|new_name| find_conflicts(root, new_name, &occurrences, &declarations, table))
.unwrap_or_default();
Some((Binding { name, occurrences }, conflicts))
}
pub(crate) fn identifier_at(source: &SourceFile, byte: usize) -> Option<String> {
let (table, tree) = parse_source(source)?;
let root = tree.root_node();
let lookup = byte.min(source.text.len().saturating_sub(1));
let leaf = identifier_leaf(root.descendant_for_byte_range(lookup, lookup)?, table)?;
leaf.utf8_text(source.text.as_bytes())
.ok()
.map(str::to_owned)
}
pub(crate) struct CrossFileMatches {
pub(crate) occurrences: Vec<(usize, usize)>,
pub(crate) conflicts: Vec<usize>,
}
pub(crate) fn cross_file_matches(
source: &SourceFile,
name: &str,
new_name: &str,
) -> Option<CrossFileMatches> {
let (table, tree) = parse_source(source)?;
let root = tree.root_node();
let src = source.text.as_bytes();
let mut declarations = Vec::new();
collect_declarations(root, src, table, &mut declarations);
let mut matches = CrossFileMatches {
occurrences: Vec::new(),
conflicts: Vec::new(),
};
collect_free_occurrences(
root,
src,
table,
name,
new_name,
&declarations,
&mut matches,
);
matches.occurrences.sort_by_key(|&(start, _)| start);
matches.conflicts.sort_unstable();
Some(matches)
}
#[allow(clippy::too_many_arguments)]
fn collect_free_occurrences(
node: Node<'_>,
src: &[u8],
table: &BindingTable,
name: &str,
new_name: &str,
declarations: &[Declaration<'_>],
out: &mut CrossFileMatches,
) {
if is_identifier_kind(node.kind(), table)
&& node.child_count() == 0
&& (table.is_reference)(node)
&& node.utf8_text(src) == Ok(name)
&& resolve_node(node, name, declarations, table).is_none()
{
out.occurrences.push((node.start_byte(), node.end_byte()));
if resolve_node(node, new_name, declarations, table).is_some() {
out.conflicts.push(node.start_byte());
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_free_occurrences(child, src, table, name, new_name, declarations, out);
}
}
fn find_conflicts(
root: Node<'_>,
new_name: &str,
occurrences: &[Occurrence],
declarations: &[Declaration<'_>],
table: &BindingTable,
) -> Vec<Conflict> {
let mut conflicts = Vec::new();
for occurrence in occurrences {
let byte = occurrence.start_byte;
let Some(node) = root.descendant_for_byte_range(byte, byte) else {
continue;
};
if resolve_node(node, new_name, declarations, table).is_some() {
conflicts.push(Conflict {
byte,
reason: format!("`{new_name}` already resolves to a binding here"),
});
}
}
conflicts
}
struct Declaration<'tree> {
name: String,
ident: Node<'tree>,
scope: usize,
}
fn scope_of(node: Node<'_>, table: &BindingTable) -> usize {
let mut current = if (table.escapes_scope)(node) {
enclosing_scope(node, table).and_then(|scope| scope.parent())
} else {
node.parent()
};
while let Some(parent) = current {
if table.scope_kinds.contains(&parent.kind()) && !(table.binds_past)(node, parent.kind()) {
return parent.id();
}
current = parent.parent();
}
node_root(node).id()
}
fn enclosing_scope<'tree>(node: Node<'tree>, table: &BindingTable) -> Option<Node<'tree>> {
let mut current = node.parent();
while let Some(parent) = current {
if table.scope_kinds.contains(&parent.kind()) {
return Some(parent);
}
current = parent.parent();
}
None
}
fn node_root(node: Node<'_>) -> Node<'_> {
let mut current = node;
while let Some(parent) = current.parent() {
current = parent;
}
current
}
fn collect_declarations<'tree>(
node: Node<'tree>,
src: &[u8],
table: &BindingTable,
out: &mut Vec<Declaration<'tree>>,
) {
for ident in (table.declared_idents)(node, src) {
if let Ok(name) = ident.utf8_text(src) {
out.push(Declaration {
name: name.to_owned(),
ident,
scope: scope_of(ident, table),
});
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_declarations(child, src, table, out);
}
}
fn resolve_node(
node: Node<'_>,
name: &str,
declarations: &[Declaration<'_>],
table: &BindingTable,
) -> Option<usize> {
let use_start = node.start_byte();
let start = if (table.escapes_scope)(node) {
enclosing_scope(node, table).and_then(|scope| scope.parent())?
} else {
node
};
let mut scope = Some(start);
let mut left_innermost_scope = false;
while let Some(current) = scope {
let is_scope = current.parent().is_none() || table.scope_kinds.contains(¤t.kind());
let hidden_class =
left_innermost_scope && table.class_scope_kinds.contains(¤t.kind());
if !hidden_class {
let scoped = declarations.iter().filter(|declaration| {
declaration.name == name && declaration.scope == current.id()
});
let resolved = match table.resolution {
Resolution::Lexical => scoped
.filter(|declaration| declaration.ident.start_byte() <= use_start)
.max_by_key(|declaration| declaration.ident.start_byte()),
Resolution::Hoisted => {
scoped.min_by_key(|declaration| declaration.ident.start_byte())
}
};
if let Some(declaration) = resolved {
return Some(declaration.ident.start_byte());
}
}
if is_scope {
left_innermost_scope = true;
}
scope = current.parent();
}
None
}
#[allow(clippy::too_many_arguments)]
fn collect_occurrences(
node: Node<'_>,
src: &[u8],
table: &BindingTable,
name: &str,
target_def: usize,
declarations: &[Declaration<'_>],
out: &mut Vec<Occurrence>,
) {
if is_identifier_kind(node.kind(), table)
&& node.child_count() == 0
&& (table.is_reference)(node)
&& node.utf8_text(src) == Ok(name)
{
let resolved = resolve_node(node, name, declarations, table);
let kind = if resolved == Some(target_def) {
if node.start_byte() == target_def {
OccurrenceKind::Definition
} else {
OccurrenceKind::Reference
}
} else {
OccurrenceKind::Shadowed
};
out.push(Occurrence {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
kind,
});
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_occurrences(child, src, table, name, target_def, declarations, out);
}
}
fn identifier_leaf<'tree>(node: Node<'tree>, table: &BindingTable) -> Option<Node<'tree>> {
let mut current = node;
while current.named_child_count() > 0 {
let byte = current.start_byte();
match current.named_descendant_for_byte_range(byte, byte) {
Some(child) if child.id() != current.id() => current = child,
_ => break,
}
}
(is_identifier_kind(current.kind(), table) && (table.is_reference)(current)).then_some(current)
}
fn is_identifier_kind(kind: &str, table: &BindingTable) -> bool {
table.identifier_kinds.contains(&kind)
}
#[derive(Clone, Copy)]
enum Resolution {
Lexical,
Hoisted,
}
struct BindingTable {
languages: &'static [Language],
scope_kinds: &'static [&'static str],
class_scope_kinds: &'static [&'static str],
identifier_kinds: &'static [&'static str],
declared_idents: for<'a, 'b> fn(Node<'a>, &'b [u8]) -> Vec<Node<'a>>,
resolution: Resolution,
is_reference: fn(Node<'_>) -> bool,
escapes_scope: fn(Node<'_>) -> bool,
binds_past: fn(Node<'_>, &str) -> bool,
}
fn any_reference(_node: Node<'_>) -> bool {
true
}
fn never_escapes(_node: Node<'_>) -> bool {
false
}
fn never_binds_past(_node: Node<'_>, _scope_kind: &str) -> bool {
false
}
fn binding_table(language: Language) -> Option<&'static BindingTable> {
BINDING_TABLES
.iter()
.find(|table| table.languages.contains(&language))
}
static BINDING_TABLES: &[BindingTable] = &[
BindingTable {
languages: &[Language::Rust],
scope_kinds: &[
"block",
"function_item",
"closure_expression",
"match_arm",
"for_expression",
"while_let_expression",
"if_let_expression",
],
class_scope_kinds: &[],
identifier_kinds: &["identifier"],
declared_idents: rust_declared_idents,
resolution: Resolution::Lexical,
is_reference: any_reference,
escapes_scope: never_escapes,
binds_past: never_binds_past,
},
BindingTable {
languages: &[Language::C, Language::Cpp],
scope_kinds: &[
"compound_statement",
"function_definition",
"for_statement",
"for_range_loop",
"lambda_expression",
],
class_scope_kinds: &[],
identifier_kinds: &["identifier"],
declared_idents: c_declared_idents,
resolution: Resolution::Lexical,
is_reference: any_reference,
escapes_scope: never_escapes,
binds_past: never_binds_past,
},
BindingTable {
languages: &[Language::Python],
scope_kinds: &[
"function_definition",
"lambda",
"class_definition",
"list_comprehension",
"set_comprehension",
"dictionary_comprehension",
"generator_expression",
],
class_scope_kinds: &["class_definition"],
identifier_kinds: &["identifier"],
declared_idents: python_declared_idents,
resolution: Resolution::Hoisted,
is_reference: python_is_reference,
escapes_scope: python_escapes_scope,
binds_past: python_binds_past,
},
];
fn rust_declared_idents<'tree>(node: Node<'tree>, _src: &[u8]) -> Vec<Node<'tree>> {
let mut out = Vec::new();
match node.kind() {
"let_declaration" | "for_expression" => {
if let Some(pattern) = node.child_by_field_name("pattern") {
collect_pattern_idents(pattern, &mut out);
}
}
"parameter" | "closure_parameters" => {
if let Some(pattern) = node.child_by_field_name("pattern") {
collect_pattern_idents(pattern, &mut out);
} else {
collect_pattern_idents(node, &mut out);
}
}
_ => {}
}
out
}
fn collect_pattern_idents<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>) {
if node.kind() == "identifier" {
out.push(node);
return;
}
if matches!(node.kind(), "scoped_identifier" | "type_identifier") {
return;
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_pattern_idents(child, out);
}
}
fn c_declared_idents<'tree>(node: Node<'tree>, _src: &[u8]) -> Vec<Node<'tree>> {
let mut out = Vec::new();
match node.kind() {
"declaration" | "parameter_declaration" => {
let mut cursor = node.walk();
for child in node.children_by_field_name("declarator", &mut cursor) {
if let Some(ident) = c_declarator_ident(child) {
out.push(ident);
}
}
}
_ => {}
}
out
}
fn c_declarator_ident(node: Node<'_>) -> Option<Node<'_>> {
match node.kind() {
"identifier" => Some(node),
"function_declarator" => None,
_ => c_declarator_ident(node.child_by_field_name("declarator")?),
}
}
fn python_is_reference(node: Node<'_>) -> bool {
let Some(parent) = node.parent() else {
return true;
};
match parent.kind() {
"attribute" => parent.child_by_field_name("attribute") != Some(node),
"keyword_argument" => parent.child_by_field_name("name") != Some(node),
_ => true,
}
}
fn python_declared_idents<'tree>(node: Node<'tree>, src: &[u8]) -> Vec<Node<'tree>> {
let mut out = Vec::new();
match node.kind() {
"parameters" | "lambda_parameters" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
python_param_ident(child, &mut out);
}
}
"function_definition" | "class_definition" => {
if let Some(name) = node.child_by_field_name("name") {
out.push(name);
}
}
"assignment" | "for_statement" | "for_in_clause" | "named_expression" | "as_pattern" => {
let field = match node.kind() {
"named_expression" => "name",
"as_pattern" => "alias",
_ => "left",
};
if let Some(target) = node.child_by_field_name(field) {
python_target_idents(target, &mut out);
}
}
_ => {}
}
out.retain(|ident| !python_freed(*ident, src));
out
}
fn python_freed(ident: Node<'_>, src: &[u8]) -> bool {
let Ok(name) = ident.utf8_text(src) else {
return false;
};
let mut current = ident.parent();
while let Some(parent) = current {
if matches!(parent.kind(), "function_definition" | "lambda") {
return python_body_declares_freed(parent, name, src);
}
current = parent.parent();
}
false
}
fn python_body_declares_freed(node: Node<'_>, name: &str, src: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"global_statement" | "nonlocal_statement" => {
let mut inner = child.walk();
if child
.children(&mut inner)
.any(|id| id.kind() == "identifier" && id.utf8_text(src) == Ok(name))
{
return true;
}
}
"function_definition" | "lambda" | "class_definition" => {}
_ => {
if python_body_declares_freed(child, name, src) {
return true;
}
}
}
}
false
}
fn python_escapes_scope(node: Node<'_>) -> bool {
python_is_definition_name(node)
|| python_in_param_default(node)
|| python_in_leading_iterable(node)
}
fn python_is_definition_name(node: Node<'_>) -> bool {
node.parent().is_some_and(|parent| {
matches!(parent.kind(), "function_definition" | "class_definition")
&& parent.child_by_field_name("name") == Some(node)
})
}
fn python_binds_past(node: Node<'_>, scope_kind: &str) -> bool {
matches!(
scope_kind,
"list_comprehension"
| "set_comprehension"
| "dictionary_comprehension"
| "generator_expression"
) && python_is_walrus_target(node)
}
fn python_is_walrus_target(node: Node<'_>) -> bool {
node.parent().is_some_and(|parent| {
parent.kind() == "named_expression" && parent.child_by_field_name("name") == Some(node)
})
}
fn python_in_param_default(node: Node<'_>) -> bool {
let start = node.start_byte();
let mut current = node.parent();
while let Some(parent) = current {
match parent.kind() {
"default_parameter" | "typed_default_parameter" => {
return parent
.child_by_field_name("value")
.is_some_and(|value| value.start_byte() <= start && start < value.end_byte());
}
"function_definition"
| "lambda"
| "list_comprehension"
| "set_comprehension"
| "dictionary_comprehension"
| "generator_expression" => return false,
_ => {}
}
current = parent.parent();
}
false
}
fn python_in_leading_iterable(node: Node<'_>) -> bool {
let start = node.start_byte();
let mut current = node.parent();
while let Some(parent) = current {
match parent.kind() {
"for_in_clause" => {
let Some(right) = parent.child_by_field_name("right") else {
return false;
};
if !(right.start_byte() <= start && start < right.end_byte()) {
return false;
}
let Some(comprehension) = parent.parent() else {
return false;
};
let mut cursor = comprehension.walk();
return comprehension
.children(&mut cursor)
.find(|child| child.kind() == "for_in_clause")
.is_some_and(|first| first.id() == parent.id());
}
"function_definition"
| "lambda"
| "list_comprehension"
| "set_comprehension"
| "dictionary_comprehension"
| "generator_expression" => return false,
_ => {}
}
current = parent.parent();
}
false
}
fn python_param_ident<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>) {
match node.kind() {
"identifier" => out.push(node),
"default_parameter" | "typed_default_parameter" => {
if let Some(name) = node.child_by_field_name("name") {
python_target_idents(name, out);
}
}
"typed_parameter" | "list_splat_pattern" | "dictionary_splat_pattern" => {
let mut cursor = node.walk();
if let Some(ident) = node
.named_children(&mut cursor)
.find(|child| child.kind() == "identifier")
{
out.push(ident);
}
}
_ => {}
}
}
fn python_target_idents<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>) {
match node.kind() {
"identifier" => out.push(node),
"pattern_list"
| "tuple_pattern"
| "list_pattern"
| "as_pattern_target"
| "list_splat_pattern"
| "dictionary_splat_pattern" => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
python_target_idents(child, out);
}
}
_ => {}
}
}