use crate::model::FileModel;
#[derive(PartialEq, Debug, serde::Serialize, serde::Deserialize)]
pub struct NeverUsedOffense {
pub line: usize,
pub column: usize,
pub name: String,
}
pub fn analyze(fm: &FileModel) -> Vec<NeverUsedOffense> {
let mut out: Vec<NeverUsedOffense> = fm
.scopes
.iter()
.flat_map(|scope| scope.entries.iter())
.filter_map(|(name, e)| dead_binding(fm, name, e))
.collect();
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
}
fn dead_binding(fm: &FileModel, name: &str, e: &crate::model::Entry) -> Option<NeverUsedOffense> {
if !e.reads.is_empty() || e.writes.is_empty() {
return None;
}
let first = e.writes.iter().map(|w| w.byte).min().unwrap_or(0);
let (line, column) = fm.line_col(first);
Some(NeverUsedOffense {
line,
column,
name: name.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::build_from_str;
fn flags(src: &str) -> Vec<NeverUsedOffense> {
analyze(&build_from_str(src))
}
#[test]
fn shorthand_hash_arg_counts_as_read() {
let f = flags("def k\n user = compute\n g(user:)\nend\n");
assert!(f.is_empty(), "shorthand key is a read: {f:?}");
}
#[test]
fn dead_local_is_flagged_at_write_line() {
let f = flags("def k\n gone = compute\n p :done\nend\n");
assert_eq!(f.len(), 1);
assert_eq!(f[0].name, "gone");
assert_eq!(f[0].line, 2);
}
#[test]
fn multiple_writes_without_reads_reported_once() {
let f = flags("def k\n x = 1\n x += 2\nend\n");
assert_eq!(f.len(), 1);
assert_eq!(f[0].line, 2);
}
#[test]
fn read_variable_not_flagged() {
let f = flags("def k\n x = 1\n p x\nend\n");
assert!(f.is_empty());
}
#[test]
fn underscore_names_exempt() {
let f = flags("def k\n _tmp = 1\nend\n");
assert!(f.is_empty());
}
#[test]
fn unused_rescue_variable_flagged() {
let f = flags("def k\n risky\nrescue => e\n :handled\nend\n");
assert_eq!(f.len(), 1);
assert_eq!(f[0].name, "e");
}
#[test]
fn parameters_never_flagged() {
let f = flags("def k(unused_arg)\n :body\nend\n");
assert!(f.is_empty());
}
}