use std::collections::HashMap;
use tree_sitter::Node;
use super::{Entry, IntroKind, Scope, Write};
use crate::never_used::NeverUsedOffense;
use crate::used_once::UsedOnceOffense;
pub struct Semantics {
pub pure: fn(Node) -> bool,
pub veto: &'static [&'static str],
pub owners: &'static [&'static str],
pub include_root_scope: bool,
}
pub fn used_once_offenses(
root: Node,
line_col: &dyn Fn(usize) -> (usize, usize),
scopes: &[Scope],
sem: &Semantics,
) -> Vec<UsedOnceOffense> {
let nodes = index_nodes(root);
let mut out = Vec::new();
for scope in scopes {
if !sem.include_root_scope && scope.kind == super::ScopeKind::Root {
continue;
}
for (name, e) in &scope.entries {
if let Some(offense) = candidate_offense(name, e, &nodes, sem, line_col) {
out.push(offense);
}
}
}
finish(out)
}
fn candidate_offense(
name: &str,
e: &super::Entry,
nodes: &HashMap<usize, Node>,
sem: &Semantics,
line_col: &dyn Fn(usize) -> (usize, usize),
) -> Option<UsedOnceOffense> {
let w = candidate(e)?;
let rhs_id = w.rhs?;
let (rhs, write_node) = match (nodes.get(&rhs_id), nodes.get(&w.node_id)) {
(Some(r), Some(w)) => (*r, *w),
_ => return None,
};
if !(sem.pure)(rhs) || !straight_line(write_node, sem) {
return None;
}
let (line, column) = line_col(w.byte);
Some(UsedOnceOffense {
line,
column,
name: name.to_string(),
})
}
fn candidate(e: &Entry) -> Option<&Write> {
if e.intro_kind != IntroKind::Assign || e.writes.len() != 1 || e.reads.len() != 1 {
return None;
}
let w = &e.writes[0];
(w.plain && e.reads[0] > w.byte).then_some(w)
}
fn straight_line(write_node: Node, sem: &Semantics) -> bool {
let mut cur = Some(write_node);
while let Some(n) = cur {
if sem.veto.contains(&n.kind()) {
return false;
}
if sem.owners.contains(&n.kind()) {
return true;
}
cur = n.parent();
}
true
}
pub fn never_used_offenses(
line_col: &dyn Fn(usize) -> (usize, usize),
scopes: &[Scope],
sem: &Semantics,
) -> Vec<NeverUsedOffense> {
let mut out = Vec::new();
for scope in scopes {
if !sem.include_root_scope && scope.kind == super::ScopeKind::Root {
continue;
}
for (name, e) in &scope.entries {
if !e.reads.is_empty() || e.writes.is_empty() {
continue;
}
let first = e.writes.iter().map(|w| w.byte).min().unwrap_or(0);
let (line, column) = line_col(first);
out.push(NeverUsedOffense {
line,
column,
name: name.to_string(),
});
}
}
finish(out)
}
fn finish<T>(mut out: Vec<T>) -> Vec<T>
where
T: HasPos + PartialEq,
{
out.sort_by_key(|o| (o.line(), o.column()));
out.dedup_by(|a, b| a.line() == b.line() && a.column() == b.column() && a.name() == b.name());
out
}
trait HasPos {
fn line(&self) -> usize;
fn column(&self) -> usize;
fn name(&self) -> &str;
}
macro_rules! impl_has_pos {
($t:ty) => {
impl HasPos for $t {
fn line(&self) -> usize {
self.line
}
fn column(&self) -> usize {
self.column
}
fn name(&self) -> &str {
&self.name
}
}
};
}
impl_has_pos!(UsedOnceOffense);
impl_has_pos!(NeverUsedOffense);
fn index_nodes<'t>(root: Node<'t>) -> HashMap<usize, Node<'t>> {
let mut map = HashMap::new();
rec(root, &mut map);
map
}
fn rec<'t>(n: Node<'t>, map: &mut HashMap<usize, Node<'t>>) {
map.insert(n.id(), n);
let mut cursor = n.walk();
for child in n.children(&mut cursor) {
rec(child, map);
}
}