Basic `blame` variable functionality

Prints the spans of the source contents for non-incremental variables.

Change-Id: I0ad62d2326591e26414bae69978632816dd3e582
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra/build/drydock/+/2464718
diff --git a/Cargo.toml b/Cargo.toml
index 6848cc9..d71ddc6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@
 nom_locate = "2.0.0"
 regex = "1.3.9"
 rental = "0.5.4"
+source-span = "2.2.0"
 walkdir = "2.3.1"
 
 [dependencies.clap]
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index a78e1fd..f145fff 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -1,7 +1,8 @@
-use std::fmt::Write;
+use std::{cmp::max, collections::HashMap, fmt::Write, path::PathBuf};
 use std::{collections::HashSet, str::FromStr};
 
 use clap::ArgMatches;
+use source_span::{fmt::Style, Position};
 
 use crate::{graph, portage::profile::is_incremental_variable, portage::profile_parser::Span};
 
@@ -59,6 +60,22 @@
     Ok(())
 }
 
+pub fn blame(config: &config::Config, sub_args: &ArgMatches) -> anyhow::Result<()> {
+    let target = sub_args.value_of("profile").unwrap();
+    let profile = ProfileKey::from_str(target)?;
+
+    let target_var = sub_args.value_of("variable").unwrap();
+    let overlay_table = build_overlay_map(&config)?;
+    if is_incremental_variable(target_var) {
+        unimplemented!();
+    } else {
+        let vals = overlay_table.compute_variable(&profile, target_var)?;
+        blame_format(&vals, config);
+    }
+
+    Ok(())
+}
+
 pub fn dump_debug(config: &config::Config, sub_args: &ArgMatches) -> anyhow::Result<()> {
     let target = sub_args.value_of("overlay").unwrap();
     let overlay_table = build_overlay_map(&config)?;
@@ -75,3 +92,58 @@
     }
     output
 }
+
+fn blame_format(tokens: &[Span], config: &config::Config) {
+    let mut seen = HashMap::new();
+    for t in tokens {
+        let idx = seen.len();
+        seen.entry(t.extra).or_insert(idx);
+    }
+    let mut f = source_span::fmt::Formatter::new();
+    f.set_viewbox(None);
+    f.hide_line_numbers();
+
+    let metrics = source_span::DEFAULT_METRICS;
+    let src_buf: source_span::SourceBuffer<(), _, _> = source_span::SourceBuffer::new(
+        tokens
+            .into_iter()
+            .flat_map(|t| t.fragment().chars().map(|c| Ok(c))),
+        Position::default(),
+        metrics,
+    );
+
+    let total_len: usize = tokens
+        .into_iter()
+        .map(|t| t.fragment().chars().count())
+        .sum();
+    let mut chars_seen: usize = 0;
+
+    for t in tokens {
+        let token_len = t.fragment().chars().count();
+        let span = source_span::Span::new(
+            Position::new(0, chars_seen),
+            Position::new(0, chars_seen + token_len),
+            Position::new(0, max(chars_seen + token_len + 1, total_len)),
+        );
+        chars_seen += token_len;
+        f.add(span, Some(span_label(t, config)), Style::Help);
+    }
+    let display_span = source_span::Span::new(
+        Position::new(0, 0),
+        Position::new(0, chars_seen),
+        Position::new(0, chars_seen + 1),
+    );
+    let formatted = f.render(src_buf.iter(), display_span, &metrics).unwrap();
+    println!("{}", formatted);
+}
+
+fn span_label(p: &Span, config: &config::Config) -> String {
+    for common_path in config.get_array("overlay_paths").unwrap() {
+        let common_prefix: PathBuf = PathBuf::from(common_path.into_str().unwrap());
+        if let Ok(new_path) = p.extra.strip_prefix(common_prefix) {
+            return format!("{}:L{}", new_path.display(), p.location_line());
+        }
+    }
+
+    format!("{}:L{}", p.extra.display(), p.location_line())
+}
diff --git a/src/main.rs b/src/main.rs
index 995105d..44c2c8b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -61,6 +61,24 @@
                 ),
         )
         .subcommand(
+            SubCommand::with_name("blame")
+                .about("Show the value of a variable for a profile annotated with the sources of that variable's contents.")
+                .arg(
+                    Arg::with_name("profile")
+                        .short("p")
+                        .long("profile")
+                        .takes_value(true)
+                        .required(true)
+                        .help("The target profile to query."),
+                )
+                .arg(
+                    Arg::with_name("variable")
+                        .takes_value(true)
+                        .required(true)
+                        .multiple(false),
+                ),
+        )
+        .subcommand(
             SubCommand::with_name("dump_debug")
                 .about("Dump debug information for an overlay.")
                 .arg(
@@ -74,16 +92,13 @@
         )
         .get_matches();
 
-    if let Some(sub_args) = args.subcommand_matches("dump_debug") {
-        commands::dump_debug(&config, sub_args)?;
+    match args.subcommand() {
+        ("blame", Some(sub_args)) => commands::blame(&config, sub_args)?,
+        ("dump_debug", Some(sub_args)) => commands::dump_debug(&config, sub_args)?,
+        ("eval", Some(sub_args)) => commands::eval(&config, sub_args)?,
+        ("parents", Some(sub_args)) => commands::parents(&config, sub_args)?,
+        _ => unimplemented!(),
     };
-    if let Some(sub_args) = args.subcommand_matches("parents") {
-        commands::parents(&config, sub_args)?;
-    };
-
-    if let Some(sub_args) = args.subcommand_matches("eval") {
-        commands::eval(&config, sub_args)?;
-    }
 
     Ok(())
 }
diff --git a/src/portage/overlay/mod.rs b/src/portage/overlay/mod.rs
index 63b8620..5824c15 100644
--- a/src/portage/overlay/mod.rs
+++ b/src/portage/overlay/mod.rs
@@ -91,12 +91,13 @@
                                 )
                             }) {
                             Ok(p) => p,
-                            Err(e) => {
-                                eprintln!(
-                                    "Malformed profile found at {:?}\n\tProblem: {}",
-                                    entry.path(),
-                                    e
-                                );
+                            Err(_e) => {
+                                // TODO: Replace with logging.
+                                // eprintln!(
+                                //     "Malformed profile found at {:?}\n\tProblem: {}",
+                                //     entry.path(),
+                                //     e
+                                // );
                                 continue;
                             }
                         };
diff --git a/src/portage/overlay/traversal.rs b/src/portage/overlay/traversal.rs
index 7bfef0e..cb4e6e6 100644
--- a/src/portage/overlay/traversal.rs
+++ b/src/portage/overlay/traversal.rs
@@ -60,6 +60,7 @@
 }
 
 impl<'a> ProfileIter<'a> {
+    #[allow(dead_code)]
     pub(super) fn new(
         overlay_table: &'a OverlayTable,
         start: <&'a OverlayTable as GraphBase>::NodeId,