use std::rc::Rc;
use super::js_modules::BindingSites;
use super::{Accounting, NameRow, QueryError, is_word_char, language_family};
#[derive(Clone, Copy)]
pub(super) struct Binding {
at: (usize, usize),
body_end: Option<(usize, usize)>,
}
const MAX_LIST: usize = 400;
const MAX_RETURN_TYPE: usize = 200;
const ARROW_KEYWORDS: &[&str] = &["return", "throw", "yield", "else", "do"];
struct Level<'a> {
open: u8,
want: bool,
skip: bool,
inert: bool,
pending: Option<&'a str>,
}
impl Accounting<'_> {
pub(super) fn js_binding_line(
&self,
row: &NameRow,
name: &str,
) -> Result<Option<usize>, QueryError> {
if language_family(&row.language) != "js" || row.hidden_receiver || row.kind == "type_usage"
{
return Ok(None);
}
let head = match &row.chain {
Some(chain) => chain.first().map_or(name, String::as_str),
None if !row.has_qualifier() => name,
None => return Ok(None),
};
self.nearest_binding(row, head, |_| true)
}
pub(super) fn js_enclosing_binding_line(
&self,
row: &NameRow,
name: &str,
) -> Result<Option<usize>, QueryError> {
if language_family(&row.language) != "js" || row.hidden_receiver || !row.is_bare_use() {
return Ok(None);
}
let site = (row.site_line, row.site_column.unwrap_or(usize::MAX));
self.nearest_binding(row, name, |binding| {
binding.body_end.is_some_and(|end| site < end)
})
}
fn nearest_binding(
&self,
row: &NameRow,
name: &str,
holds: impl Fn(&Binding) -> bool,
) -> Result<Option<usize>, QueryError> {
let bindings = self.js_binding_sites(&row.path, name);
if bindings.is_empty() {
return Ok(None);
}
let caller_start = match row.caller_id.as_deref() {
Some(id) => self
.definition_by_id(id)?
.map_or(1, |caller| caller.start_line),
None => 1,
};
let site = (row.site_line, row.site_column.unwrap_or(usize::MAX));
Ok(bindings
.iter()
.rev()
.find(|binding| (caller_start, 0) <= binding.at && binding.at < site && holds(binding))
.map(|binding| binding.at.0))
}
fn js_binding_sites(&self, path: &str, name: &str) -> BindingSites {
let key = (path.to_string(), name.to_string());
if let Some(sites) = self.js.bindings.borrow().get(&key) {
return sites.clone();
}
let sites = Rc::new(
std::fs::read_to_string(self.root.join(path))
.map(|text| binding_sites(&text, name))
.unwrap_or_default(),
);
self.js.bindings.borrow_mut().insert(key, sites.clone());
sites
}
}
fn binding_sites(text: &str, name: &str) -> Vec<Binding> {
let line_starts: Vec<usize> = std::iter::once(0)
.chain(text.match_indices('\n').map(|(at, _)| at + 1))
.collect();
let position = |at: usize| {
let line = line_starts.partition_point(|&start| start <= at);
(line, at - line_starts[line - 1])
};
text.match_indices(name)
.filter_map(|(at, _)| {
let after = binding_end(text, at, name)?;
Some(Binding {
at: position(at),
body_end: block_body_end(text, after).map(position),
})
})
.collect()
}
fn binding_end(text: &str, at: usize, name: &str) -> Option<usize> {
let end = at + name.len();
let before = text[..at].trim_end();
let written_as_word =
!text[..at].ends_with(is_word_char) && !text[end..].starts_with(is_word_char);
let member = before.ends_with('.') && !before.ends_with("...");
if !written_as_word || member {
return None;
}
if text[end..].trim_start().starts_with("=>") {
return Some(end);
}
let open = enclosing_open(text, at)?;
let close = matching_close(text, open)?;
declared_region(text, open, close)
.filter(|region| declares(region, name))
.map(|_| close + 1)
}
fn block_body_end(text: &str, after: usize) -> Option<usize> {
let rest = text[after..].trim_start();
let rest = rest.strip_prefix("=>").map_or(rest, str::trim_start);
if !rest.starts_with('{') {
return None;
}
brace_close(text, text.len() - rest.len())
}
fn brace_close(text: &str, open: usize) -> Option<usize> {
let bytes = text.as_bytes();
let end = bytes.len();
let mut depth = 0;
let mut at = open;
while at < end {
if let Some(after) = comment_end(bytes, at, end) {
at = after;
continue;
}
match bytes[at] {
b'\'' | b'"' | b'`' => at = quote_end(bytes, at, end)?,
b'{' => depth += 1,
b'}' if depth == 1 => return Some(at),
b'}' => depth -= 1,
_ => {}
}
at += 1;
}
None
}
fn enclosing_open(text: &str, at: usize) -> Option<usize> {
let start = at.saturating_sub(MAX_LIST);
let mut depth = 0;
for (offset, byte) in text.as_bytes()[start..at].iter().enumerate().rev() {
match byte {
b')' | b']' | b'}' => depth += 1,
b'(' | b'[' | b'{' if depth > 0 => depth -= 1,
b'(' => return Some(start + offset),
_ => {}
}
}
None
}
fn matching_close(text: &str, open: usize) -> Option<usize> {
let mut depth = 0;
for (offset, byte) in text.as_bytes()[open + 1..]
.iter()
.take(MAX_LIST)
.enumerate()
{
match byte {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' if depth > 0 => depth -= 1,
b')' => return Some(open + 1 + offset),
b']' | b'}' => return None,
_ => {}
}
}
None
}
fn ends_with_word(text: &str, word: &str) -> bool {
text.strip_suffix(word)
.is_some_and(|rest| !rest.ends_with(is_word_char))
}
fn ends_with_keyword(text: &str, word: &str) -> bool {
ends_with_word(text, word) && !text[..text.len() - word.len()].trim_end().ends_with('.')
}
fn returns_to_arrow(text: &str) -> bool {
let bytes = text.as_bytes();
let end = bytes.len().min(MAX_RETURN_TYPE);
let mut depth = 0;
let mut last = None;
let mut at = 0;
while at < end {
if let Some(after) = comment_end(bytes, at, end) {
at = after;
continue;
}
let byte = bytes[at];
match byte {
b'\'' | b'"' | b'`' => match quote_end(bytes, at, end) {
Some(close) => at = close,
None => return false,
},
b'=' if bytes.get(at + 1) == Some(&b'>') => {
if depth == 0 {
return true;
}
at += 1;
}
b'(' | b'{' | b'[' | b'<' => depth += 1,
b')' | b'}' | b']' | b'>' if depth == 0 => return false,
b')' | b'}' | b']' | b'>' => depth -= 1,
b';' | b'?' | b',' if depth == 0 => return false,
b'\n' if depth == 0 && !type_continues(last, &text[at + 1..]) => return false,
_ => {}
}
if !byte.is_ascii_whitespace() {
last = Some(byte);
}
at += 1;
}
false
}
fn comment_end(bytes: &[u8], at: usize, end: usize) -> Option<usize> {
let rest = &bytes[at..end];
match rest {
[b'/', b'/', ..] => Some(
at + rest
.iter()
.position(|&byte| byte == b'\n')
.unwrap_or(rest.len()),
),
[b'/', b'*', ..] => Some(
rest[2..]
.windows(2)
.position(|pair| pair == b"*/")
.map_or(end, |close| at + close + 4),
),
_ => None,
}
}
fn quote_end(bytes: &[u8], at: usize, end: usize) -> Option<usize> {
let mut index = at + 1;
while index < end {
match bytes[index] {
b'\\' => index += 1,
byte if byte == bytes[at] => return Some(index),
_ => {}
}
index += 1;
}
None
}
fn type_continues(last: Option<u8>, after: &str) -> bool {
matches!(last, None | Some(b'|' | b'&')) || after.trim_start().starts_with(['|', '&'])
}
fn code_before(text: &str) -> &str {
let mut before = text.trim_end();
let line = text.rfind('\n').map_or(0, |at| at + 1);
if !text[line..].trim().is_empty() {
return before;
}
loop {
let line = before.rfind('\n').map_or(0, |at| at + 1);
match before[line..].find("//") {
Some(at) => before = before[..line + at].trim_end(),
None => return before,
}
}
}
fn declared_region(text: &str, open: usize, close: usize) -> Option<&str> {
let before = code_before(&text[..open]);
let inner = &text[open + 1..close];
let after = text[close + 1..].trim_start();
let function = ends_with_keyword(before, "function")
|| before
.strip_suffix('*')
.is_some_and(|generator| ends_with_keyword(generator.trim_end(), "function"));
let call_like = before.ends_with([')', ']'])
|| (before.ends_with(is_word_char)
&& !ARROW_KEYWORDS
.iter()
.any(|word| ends_with_keyword(before, word)));
let in_expression = ["&&", "||", "??"]
.iter()
.any(|operator| before.ends_with(operator));
let typed_arrow = after
.strip_prefix(':')
.is_some_and(|returns| !in_expression && returns_to_arrow(returns))
&& (!call_like || ends_with_word(before, "async"));
if ends_with_keyword(before, "catch") || function || after.starts_with("=>") || typed_arrow {
return Some(inner);
}
let head = before.strip_suffix("await").map_or(before, str::trim_end);
if !ends_with_keyword(head, "for") {
return None;
}
let declaration = ["const", "let", "var"].iter().find_map(|keyword| {
let rest = inner.trim_start().strip_prefix(keyword)?;
rest.starts_with(char::is_whitespace).then_some(rest)
})?;
loop_pattern(declaration)
}
fn loop_pattern(declaration: &str) -> Option<&str> {
let mut depth = 0;
for (at, c) in declaration.char_indices() {
match c {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
c if c.is_whitespace() && depth == 0 => {
let rest = declaration[at..].trim_start();
let keyword = ["of", "in"].iter().find_map(|word| rest.strip_prefix(word));
if keyword.is_some_and(|after| after.starts_with(char::is_whitespace)) {
return Some(&declaration[..at]);
}
}
_ => {}
}
}
None
}
fn declares(list: &str, name: &str) -> bool {
let bytes = list.as_bytes();
let word =
|byte: u8| byte >= 0x80 || byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$';
let level = |open, want, skip, inert| Level {
open,
want,
skip,
inert,
pending: None,
};
let mut levels = vec![level(b'(', true, false, false)];
let mut at = 0;
while at < bytes.len() {
let byte = bytes[at];
at += 1;
let top = levels.last_mut().expect("the list level is never popped");
match byte {
b'\'' | b'"' | b'`' => {
while at < bytes.len() && bytes[at] != byte {
at += if bytes[at] == b'\\' { 2 } else { 1 };
}
at += 1;
}
_ if word(byte) => {
let start = at - 1;
while at < bytes.len() && word(bytes[at]) {
at += 1;
}
if top.inert || top.skip || !top.want {
continue;
}
top.want = false;
let ident = &list[start..at];
if top.open == b'{' {
top.pending = Some(ident);
} else if ident == name {
return true;
}
}
b',' if !top.inert => {
if settle(top, name) {
return true;
}
top.want = true;
top.skip = false;
}
b':' if !top.inert && !top.skip => {
if top.open == b'{' && top.pending.take().is_some() {
top.want = true;
} else {
top.skip = true;
}
}
b'=' if bytes.get(at) == Some(&b'>') => at += 1,
b'=' if !top.inert && !top.skip => {
if settle(top, name) {
return true;
}
top.skip = true;
}
b'{' | b'[' if !top.inert && !top.skip && top.want => {
top.want = false;
levels.push(level(byte, true, false, false));
}
b'{' | b'[' | b'(' => levels.push(level(byte, false, true, true)),
b'<' if top.skip => levels.push(level(byte, false, true, true)),
b'>' if top.open == b'<' => {
levels.pop();
}
b')' | b']' | b'}' if levels.len() > 1 => {
if let Some(mut closed) = levels.pop()
&& settle(&mut closed, name)
{
return true;
}
}
_ => {}
}
}
false
}
fn settle(level: &mut Level, name: &str) -> bool {
level.pending.take().is_some_and(|ident| ident == name)
}