mod builder;
mod reads;
mod scopes;
mod writes;
mod masgn;
use std::collections::HashMap;
use tree_sitter::{Node, Tree};
pub type ScopeId = usize;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IntroKind {
Assign,
Binding,
}
#[derive(Clone, Copy, Debug)]
pub struct Write {
pub byte: usize,
pub node_id: usize,
pub kind: WriteKind,
pub rhs: Option<(usize, usize)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WriteKind {
Plain,
OpAssign,
Masgn,
ForVar,
RescueVar,
}
#[derive(Clone, Copy, Debug)]
pub struct Read {
pub byte: usize,
pub under_defined: bool,
}
#[derive(Debug)]
pub struct Entry {
pub intro_byte: usize,
pub intro_kind: IntroKind,
pub writes: Vec<Write>,
pub reads: Vec<Read>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScopeKind {
Root,
Method,
ClassLike,
Block,
}
#[derive(Debug)]
pub struct ScopeData {
pub parent: Option<ScopeId>,
pub kind: ScopeKind,
pub entered_at: usize,
pub entries: HashMap<Box<str>, Entry>,
}
pub struct FileModel<'s> {
pub src: &'s [u8],
pub tree: Tree,
pub scopes: Vec<ScopeData>,
pub csend_sites: Vec<(usize, Box<str>, ScopeId)>,
pub vcall_sites: Vec<usize>,
}
impl<'s> FileModel<'s> {
pub fn text(&self, n: Node<'_>) -> &str {
n.utf8_text(self.src).unwrap_or("")
}
#[allow(dead_code)]
pub fn lookup(&self, s: ScopeId, pos: usize, name: &str) -> Option<ScopeId> {
lookup_scope(&self.scopes, s, pos, name)
}
pub fn line_col(&self, byte: usize) -> (usize, usize) {
let point = self
.tree
.root_node()
.descendant_for_byte_range(byte, byte)
.map(|n| n.start_position())
.unwrap_or_default();
(point.row + 1, point.column)
}
}
pub fn lookup_scope(
scopes: &[ScopeData],
mut s: ScopeId,
pos: usize,
name: &str,
) -> Option<ScopeId> {
let mut effective_pos = pos;
loop {
let scope = &scopes[s];
if let Some(e) = scope.entries.get(name)
&& e.intro_byte <= effective_pos
{
return Some(s);
}
let parent = scope.parent?;
if matches!(scope.kind, ScopeKind::Block) {
effective_pos = effective_pos.min(scope.entered_at);
} else {
return None;
}
s = parent;
}
}
pub fn build<'s>(src: &'s [u8], tree: Tree) -> FileModel<'s> {
let mut scopes = vec![ScopeData {
parent: None,
kind: ScopeKind::Root,
entered_at: 0,
entries: HashMap::new(),
}];
let mut csend_sites = Vec::new();
let mut vcall_sites = Vec::new();
{
let mut b = builder::Builder {
src,
scopes: &mut scopes,
csend_sites: &mut csend_sites,
vcall_sites: &mut vcall_sites,
};
b.walk(tree.root_node(), 0, false);
}
FileModel {
src,
tree,
scopes,
csend_sites,
vcall_sites,
}
}
#[cfg(test)]
pub(crate) fn build_from_str(src: &str) -> FileModel<'_> {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_ruby::LANGUAGE.into())
.expect("ruby grammar");
build(
src.as_bytes(),
parser.parse(src, None).expect("syntax tree"),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rebind_inside_block_hits_shared_outer_binding() {
assert_eq!(
build_from_str("x = 1\n[1].each { x = 2 }\n").scopes[0].entries["x"]
.writes
.len(),
2
);
}
#[test]
fn vcall_never_counts_as_variable_read() {
let fm = build_from_str("def m\n bar\nend\n");
assert!(fm.scopes.iter().all(|s| s.entries.is_empty()));
}
#[test]
fn local_read_after_introduction_is_tracked() {
let fm = build_from_str("def m\n x = 1\n p x\nend\n");
let e = fm
.scopes
.iter()
.find(|s| s.kind == ScopeKind::Method)
.expect("method scope")
.entries
.get("x")
.expect("entry");
assert_eq!((e.writes.len(), e.reads.len()), (1, 1));
}
#[test]
fn block_local_var_does_not_leak() {
let fm = build_from_str("[1].each { y = 2 }\n");
let blk = fm
.scopes
.iter()
.find(|s| s.kind == ScopeKind::Block)
.expect("block scope");
assert!(blk.entries.contains_key("y"));
assert!(!fm.scopes[0].entries.contains_key("y"));
}
}