//! The questions the index exists to answer. use anyhow::{Context, Result}; use rusqlite::{params, Connection}; use std::path::Path; pub fn open(db_path: &Path) -> Result { let conn = Connection::open(db_path).with_context(|| format!("opening {}", db_path.display()))?; conn.query_row("SELECT value FROM meta WHERE key = 'revision'", [], |r| { r.get::<_, String>(0) }) .with_context(|| format!("{} is not an xray index", db_path.display()))?; Ok(conn) } /// A reference that counts as a use. A `pub use` re-export names an item /// without using it, so a crate that only re-exports something has not kept /// it alive. const LIVE_REF: &str = " SELECT r.* FROM ref r LEFT JOIN item f ON f.id = r.from_item WHERE f.kind IS NULL OR f.kind <> 'use' "; pub fn info(conn: &Connection) -> Result<()> { let mut stmt = conn.prepare("SELECT key, value FROM meta ORDER BY key")?; let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?; for row in rows { let (k, v) = row?; println!("{k}\t{v}"); } for (label, sql) in [ ("items", "SELECT COUNT(*) FROM item"), ("refs", "SELECT COUNT(*) FROM ref"), ("crates", "SELECT COUNT(DISTINCT crate) FROM item"), ] { let n: i64 = conn.query_row(sql, [], |r| r.get(0))?; println!("{label}\t{n}"); } Ok(()) } /// Public items whose `pub` earns nothing: nothing outside the crate reaches /// them, and no test does either. pub fn dead_pub(conn: &Connection, krate: Option<&str>, all: bool) -> Result<()> { let sql = format!( " SELECT i.module_path || '::' || i.name AS path, i.kind, i.file, i.line_start, (SELECT COUNT(*) FROM ({LIVE_REF}) l WHERE l.to_item = i.id) AS n, (SELECT COUNT(DISTINCT l.file) FROM ({LIVE_REF}) l WHERE l.to_item = i.id AND l.file <> i.file) AS elsewhere FROM item i WHERE i.visibility = 'public' AND i.target_kind = 'lib' AND i.kind NOT IN ('use', 'module', 'extern_crate') AND (?1 OR i.exported = 1) AND (?2 IS NULL OR i.crate = ?2) AND NOT EXISTS ( SELECT 1 FROM ({LIVE_REF}) l WHERE l.to_item = i.id AND (l.crate <> i.crate OR l.target_kind NOT IN ('lib', 'bin') OR l.cfg_gated = 1) ) ORDER BY i.crate, i.file, i.line_start " ); let mut stmt = conn.prepare(&sql)?; let rows = stmt.query_map(params![all, krate], |r| { Ok(( r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?, r.get::<_, i64>(3)?, r.get::<_, i64>(4)?, r.get::<_, i64>(5)?, )) })?; let mut total = 0; for row in rows { let (path, kind, file, line, n, elsewhere) = row?; let verdict = match (n, elsewhere) { (0, _) => "unreferenced", (_, 0) => "file-local", _ => "crate-local", }; println!("{file}:{line}\t{kind}\t{verdict}\t{n} refs\t{path}"); total += 1; } eprintln!("{total} public items reach nothing outside their own crate"); Ok(()) } /// Every reference to an item, by full path or by any suffix of one. pub fn refs(conn: &Connection, path: &str) -> Result<()> { let mut stmt = conn.prepare( "SELECT id, module_path || '::' || name, kind, file, line_start FROM item WHERE module_path || '::' || name = ?1 OR module_path || '::' || name LIKE '%::' || ?1 ORDER BY file, line_start", )?; let found = stmt .query_map(params![path], |r| { Ok(( r.get::<_, i64>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?, r.get::<_, String>(3)?, r.get::<_, i64>(4)?, )) })? .collect::>>()?; if found.is_empty() { anyhow::bail!("no item matches {path}"); } for (id, full, kind, file, line) in found { println!("{kind} {full}\n defined at {file}:{line}"); let mut stmt = conn.prepare( "SELECT r.file, r.line, r.crate, r.target_kind, r.cfg_gated, CASE WHEN f.kind = 'use' THEN '(re-export)' ELSE COALESCE(f.module_path || '::' || f.name, '') END FROM ref r LEFT JOIN item f ON f.id = r.from_item WHERE r.to_item = ?1 ORDER BY r.file, r.line", )?; let rows = stmt.query_map(params![id], |r| { Ok(( r.get::<_, String>(0)?, r.get::<_, i64>(1)?, r.get::<_, String>(2)?, r.get::<_, String>(3)?, r.get::<_, i64>(4)?, r.get::<_, String>(5)?, )) })?; let mut n = 0; for row in rows { let (file, line, krate, target, gated, from) = row?; let tag = if gated == 1 { " [cfg-gated]" } else { "" }; println!(" {file}:{line}\t{krate} {target}{tag}\t{from}"); n += 1; } println!(" {n} references"); } Ok(()) } /// Declared workspace dependencies, each with the number of item-level /// references that actually cross that edge. pub fn crate_edges(conn: &Connection) -> Result<()> { let mut stmt = conn.prepare( " SELECT d.from_crate, d.to_crate, d.kind, (SELECT COUNT(*) FROM ref r JOIN item i ON i.id = r.to_item WHERE r.crate = d.from_crate AND i.crate = d.to_crate) AS n FROM dep d ORDER BY d.from_crate, d.to_crate ", )?; let rows = stmt.query_map([], |r| { Ok(( r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?, r.get::<_, i64>(3)?, )) })?; let mut unused = 0; for row in rows { let (from, to, kind, n) = row?; if n == 0 { unused += 1; } println!("{from}\t{to}\t{kind}\t{n}"); } eprintln!("{unused} declared dependencies carry no item-level reference"); Ok(()) } /// Every item in a file, with the span it occupies. pub fn file_items(conn: &Connection, file: &str) -> Result<()> { let mut stmt = conn.prepare( "SELECT line_start, line_end, kind, visibility, module_path || '::' || name, (SELECT COUNT(*) FROM ref r WHERE r.to_item = item.id) FROM item WHERE file = ?1 OR file LIKE '%' || ?1 ORDER BY line_start", )?; let rows = stmt.query_map(params![file], |r| { Ok(( r.get::<_, i64>(0)?, r.get::<_, i64>(1)?, r.get::<_, String>(2)?, r.get::<_, String>(3)?, r.get::<_, String>(4)?, r.get::<_, i64>(5)?, )) })?; let mut n = 0; for row in rows { let (start, end, kind, vis, path, refs) = row?; println!("{start}-{end}\t{vis} {kind}\t{refs} refs\t{path}"); n += 1; } eprintln!("{n} items"); Ok(()) }