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};
mod cpp;
mod python;
mod rust;
mod typescript;
mod vimscript;
#[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,
}
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> {
if let Some((table, tree)) = parse_source(source) {
let root = tree.root_node();
let lookup = byte.min(source.text.len().saturating_sub(1));
if let Some(leaf) = root
.descendant_for_byte_range(lookup, lookup)
.and_then(|node| identifier_leaf(node, table))
{
return leaf
.utf8_text(source.text.as_bytes())
.ok()
.map(str::to_owned);
}
}
identifier_at_byte(source.text.as_bytes(), byte)
}
fn identifier_at_byte(text: &[u8], byte: usize) -> Option<String> {
let pos = byte.min(text.len().saturating_sub(1));
if !text[pos].is_ascii_alphanumeric() && text[pos] != b'_' {
return None;
}
let mut start = pos;
while start > 0 && (text[start - 1].is_ascii_alphanumeric() || text[start - 1] == b'_') {
start -= 1;
}
if text[start].is_ascii_digit() {
return None;
}
let mut end = pos + 1;
while end < text.len() && (text[end].is_ascii_alphanumeric() || text[end] == b'_') {
end += 1;
}
String::from_utf8(text[start..end].to_vec()).ok()
}
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(
root: Node<'_>,
src: &[u8],
table: &BindingTable,
name: &str,
new_name: &str,
declarations: &[Declaration<'_>],
out: &mut CrossFileMatches,
) {
let mut stack: Vec<Node<'_>> = vec![root];
while let Some(node) = stack.pop() {
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();
let children: Vec<_> = node.children(&mut cursor).collect();
for child in children.into_iter().rev() {
stack.push(child);
}
}
}
fn find_conflicts(
root: Node<'_>,
new_name: &str,
occurrences: &[Occurrence],
declarations: &[Declaration<'_>],
table: &BindingTable,
) -> Vec<Conflict> {
occurrences
.iter()
.filter_map(|occurrence| {
let byte = occurrence.start_byte;
let node = root.descendant_for_byte_range(byte, byte)?;
resolve_node(node, new_name, declarations, table)
.is_some()
.then(|| Conflict {
byte,
reason: format!("`{new_name}` already resolves to a binding here"),
})
})
.collect()
}
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>(
root: Node<'tree>,
src: &[u8],
table: &BindingTable,
out: &mut Vec<Declaration<'tree>>,
) {
let mut stack: Vec<Node<'tree>> = vec![root];
while let Some(node) = stack.pop() {
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();
let children: Vec<_> = node.children(&mut cursor).collect();
for child in children.into_iter().rev() {
stack.push(child);
}
}
}
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(
root: Node<'_>,
src: &[u8],
table: &BindingTable,
name: &str,
target_def: usize,
declarations: &[Declaration<'_>],
out: &mut Vec<Occurrence>,
) {
let mut stack: Vec<Node<'_>> = vec![root];
while let Some(node) = stack.pop() {
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();
let children: Vec<_> = node.children(&mut cursor).collect();
for child in children.into_iter().rev() {
stack.push(child);
}
}
}
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 {
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> {
match language {
Language::Rust => Some(&RUST_TABLE),
Language::C | Language::Cpp => Some(&CPP_TABLE),
Language::Python => Some(&PYTHON_TABLE),
Language::TypeScript | Language::Tsx | Language::JavaScript | Language::Jsx => {
Some(&TYPESCRIPT_TABLE)
}
Language::Vimscript => Some(&VIMSCRIPT_TABLE),
_ => None,
}
}
static RUST_TABLE: BindingTable = BindingTable {
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,
};
static CPP_TABLE: BindingTable = BindingTable {
scope_kinds: &[
"compound_statement",
"function_definition",
"for_statement",
"for_range_loop",
"lambda_expression",
],
class_scope_kinds: &[],
identifier_kinds: &["identifier"],
declared_idents: cpp::declared_idents,
resolution: Resolution::Lexical,
is_reference: any_reference,
escapes_scope: never_escapes,
binds_past: never_binds_past,
};
static PYTHON_TABLE: BindingTable = BindingTable {
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,
};
static TYPESCRIPT_TABLE: BindingTable = BindingTable {
scope_kinds: &[
"statement_block",
"function_declaration",
"function_expression",
"generator_function_declaration",
"arrow_function",
"method_definition",
"class_declaration",
"for_statement",
"for_in_statement",
"catch_clause",
],
class_scope_kinds: &["class_declaration"],
identifier_kinds: &[
"identifier",
"shorthand_property_identifier",
"shorthand_property_identifier_pattern",
],
declared_idents: typescript::declared_idents,
resolution: Resolution::Lexical,
is_reference: typescript::is_reference,
escapes_scope: typescript::is_hoisted_name,
binds_past: never_binds_past,
};
static VIMSCRIPT_TABLE: BindingTable = BindingTable {
scope_kinds: &[
"function_definition",
"lambda_expression",
"if_statement",
"while_loop",
"for_loop",
"try_statement",
],
class_scope_kinds: &[],
identifier_kinds: &["identifier", "name"],
declared_idents: vimscript::declared_idents,
resolution: Resolution::Lexical,
is_reference: vimscript::is_reference,
escapes_scope: vimscript::escapes_scope,
binds_past: never_binds_past,
};