From c020b2d439c14f7bb826594ce40a372c2f3c98a6 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 30 Jun 2025 13:10:54 +0000 Subject: [PATCH 1/3] Fix panic due to manually created span --- crates/rust-project-goals-cli/src/updates.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rust-project-goals-cli/src/updates.rs b/crates/rust-project-goals-cli/src/updates.rs index 9be83489..baed1275 100644 --- a/crates/rust-project-goals-cli/src/updates.rs +++ b/crates/rust-project-goals-cli/src/updates.rs @@ -232,7 +232,7 @@ fn help_wanted( fn why_this_goal(issue_id: &IssueId, issue: &ExistingGithubIssue) -> anyhow::Result { let span = Span { file: issue_id.url().into(), - bytes: 0..0, + bytes: 0..issue.body.len(), }; let sections = markwaydown::parse_text(Spanned::new(&issue.body, span))?; for section in sections { From 14ad2b7d11715e050f31a0e22f45659f1b2b694e Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 23 Jun 2025 15:26:40 +0000 Subject: [PATCH 2/3] Use new spanned diagnostics --- Cargo.lock | 33 ++++++++++++++++--- crates/mdbook-goals/Cargo.toml | 1 - .../mdbook-goals/src/mdbook_preprocessor.rs | 2 +- crates/rust-project-goals-cli/Cargo.toml | 1 - crates/rust-project-goals-cli/src/updates.rs | 2 +- crates/rust-project-goals/Cargo.toml | 2 +- crates/rust-project-goals/src/goal.rs | 12 +++---- crates/rust-project-goals/src/lib.rs | 1 + crates/rust-project-goals/src/markwaydown.rs | 2 +- 9 files changed, 40 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa199e70..24c63cac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + [[package]] name = "anstream" version = "0.6.14" @@ -372,6 +382,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "comrak" version = "0.31.0" @@ -1356,7 +1375,6 @@ dependencies = [ "rust-project-goals", "semver", "serde_json", - "spanned", ] [[package]] @@ -2054,7 +2072,6 @@ dependencies = [ "rust-project-goals-json", "serde", "serde_json", - "spanned", "walkdir", ] @@ -2343,13 +2360,15 @@ dependencies = [ [[package]] name = "spanned" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4b0c055fde758f086eb4a6e73410247df8a3837fd606d2caeeaf72aa566d" +checksum = "cdb1d4d62460f8db3edc9aec4b399b348232611222f6af7d471edcde75158225" dependencies = [ + "annotate-snippets", "anyhow", "bstr", "color-eyre", + "colored", ] [[package]] @@ -2832,6 +2851,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/crates/mdbook-goals/Cargo.toml b/crates/mdbook-goals/Cargo.toml index e3640dfc..fc0864c0 100644 --- a/crates/mdbook-goals/Cargo.toml +++ b/crates/mdbook-goals/Cargo.toml @@ -11,4 +11,3 @@ regex = "1.11.1" rust-project-goals = { version = "0.1.0", path = "../rust-project-goals" } semver = "1.0.23" serde_json = "1.0.133" -spanned = "0.4.0" diff --git a/crates/mdbook-goals/src/mdbook_preprocessor.rs b/crates/mdbook-goals/src/mdbook_preprocessor.rs index 7b292651..5c2a7f3f 100644 --- a/crates/mdbook-goals/src/mdbook_preprocessor.rs +++ b/crates/mdbook-goals/src/mdbook_preprocessor.rs @@ -12,11 +12,11 @@ use rust_project_goals::config::Configuration; use rust_project_goals::format_team_ask::format_team_asks; use rust_project_goals::util::{self, GithubUserInfo}; +use rust_project_goals::spanned::Spanned; use rust_project_goals::{ goal::{self, GoalDocument, Status, TeamAsk}, re, team, }; -use spanned::Spanned; const LINKS: &str = "links"; const LINKIFIERS: &str = "linkifiers"; diff --git a/crates/rust-project-goals-cli/Cargo.toml b/crates/rust-project-goals-cli/Cargo.toml index 92e3071e..66ad46c5 100644 --- a/crates/rust-project-goals-cli/Cargo.toml +++ b/crates/rust-project-goals-cli/Cargo.toml @@ -16,4 +16,3 @@ clap = { version = "4.5.23", features = ["derive"] } rust-project-goals-json = { version = "0.1.0", path = "../rust-project-goals-json" } handlebars = { version = "6.2.0", features = ["dir_source"] } comrak = "0.31.0" -spanned = "0.4.0" diff --git a/crates/rust-project-goals-cli/src/updates.rs b/crates/rust-project-goals-cli/src/updates.rs index baed1275..74e893d5 100644 --- a/crates/rust-project-goals-cli/src/updates.rs +++ b/crates/rust-project-goals-cli/src/updates.rs @@ -3,9 +3,9 @@ use chrono::{Datelike, NaiveDate}; use regex::Regex; use rust_project_goals::markwaydown; use rust_project_goals::re::{HELP_WANTED, TLDR}; +use rust_project_goals::spanned::{Span, Spanned}; use rust_project_goals::util::comma; use rust_project_goals_json::GithubIssueState; -use spanned::{Span, Spanned}; use std::io::Write; use std::path::Path; use std::process::{Command, Stdio}; diff --git a/crates/rust-project-goals/Cargo.toml b/crates/rust-project-goals/Cargo.toml index a29578a4..ad043859 100644 --- a/crates/rust-project-goals/Cargo.toml +++ b/crates/rust-project-goals/Cargo.toml @@ -16,4 +16,4 @@ rust_team_data = { git = "https://github.com/rust-lang/team" } rust-project-goals-json = { version = "0.1.0", path = "../rust-project-goals-json" } toml = "0.8.19" indexmap = "2.7.1" -spanned = "0.4.0" +spanned = "0.5.0" diff --git a/crates/rust-project-goals/src/goal.rs b/crates/rust-project-goals/src/goal.rs index ab5787a3..2b865a46 100644 --- a/crates/rust-project-goals/src/goal.rs +++ b/crates/rust-project-goals/src/goal.rs @@ -409,8 +409,8 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { if !re::is_just(&re::USERNAME, poc_row[1].trim()) { anyhow::bail!( - "point of contact must be a single github username (found {})", - poc_row[1].render() + "point of contact must be a single github username (found {:?})", + poc_row[1] ) } @@ -538,8 +538,8 @@ fn goal_plan( })) } _ => anyhow::bail!( - "multiple goal tables found in section `{}`", - section.title.render() + "multiple goal tables found in section `{:?}`", + section.title ), } } @@ -678,8 +678,8 @@ fn expect_headers(table: &Table, expected: &[&str]) -> anyhow::Result<()> { if table.header != expected { // FIXME: do a diff so we see which headers are missing or extraneous anyhow::bail!( - "{}: unexpected table header, expected `{:?}`, found `{:?}`", - table.header[0].render(), + "{:?}: unexpected table header, expected `{:?}`, found `{:?}`", + table.header[0], expected, table.header.iter().map(|h| &h.content), ); diff --git a/crates/rust-project-goals/src/lib.rs b/crates/rust-project-goals/src/lib.rs index 48a12155..7cea3154 100644 --- a/crates/rust-project-goals/src/lib.rs +++ b/crates/rust-project-goals/src/lib.rs @@ -6,3 +6,4 @@ pub mod markwaydown; pub mod re; pub mod team; pub mod util; +pub use spanned; diff --git a/crates/rust-project-goals/src/markwaydown.rs b/crates/rust-project-goals/src/markwaydown.rs index 718b6407..c2782711 100644 --- a/crates/rust-project-goals/src/markwaydown.rs +++ b/crates/rust-project-goals/src/markwaydown.rs @@ -32,7 +32,7 @@ pub struct Table { pub fn parse(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); let text = Spanned::read_str_from_file(path).transpose()?; - parse_text(text.as_ref()) + parse_text(text.as_ref().map(|s| s.as_ref())) } pub fn parse_text(text: Spanned<&str>) -> anyhow::Result> { From 909a2b13ea3acde186b232f962ab4c1662f00354 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 23 Jun 2025 15:49:49 +0000 Subject: [PATCH 3/3] Use new spanned diagnostics everywhere --- Cargo.lock | 4 +- .../mdbook-goals/src/mdbook_preprocessor.rs | 45 ++--- crates/rust-project-goals-cli/src/cfp.rs | 29 ++-- .../src/generate_json.rs | 3 +- crates/rust-project-goals-cli/src/main.rs | 17 +- crates/rust-project-goals-cli/src/rfc.rs | 53 +++--- .../rust-project-goals-cli/src/team_repo.rs | 22 +-- crates/rust-project-goals-cli/src/updates.rs | 31 ++-- .../src/updates/templates.rs | 7 +- crates/rust-project-goals/Cargo.toml | 2 +- crates/rust-project-goals/src/config.rs | 6 +- .../rust-project-goals/src/format_team_ask.rs | 13 +- crates/rust-project-goals/src/gh/issues.rs | 71 ++++---- crates/rust-project-goals/src/gh/labels.rs | 9 +- crates/rust-project-goals/src/goal.rs | 157 +++++++++--------- crates/rust-project-goals/src/markwaydown.rs | 38 ++--- crates/rust-project-goals/src/team.rs | 23 +-- crates/rust-project-goals/src/util.rs | 12 +- 18 files changed, 271 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24c63cac..0e0eaf09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2360,9 +2360,9 @@ dependencies = [ [[package]] name = "spanned" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb1d4d62460f8db3edc9aec4b399b348232611222f6af7d471edcde75158225" +checksum = "118662960e1508d35b4162b0f0e41828e6d8e3c7c07da7d959c18e803528d1a2" dependencies = [ "annotate-snippets", "anyhow", diff --git a/crates/mdbook-goals/src/mdbook_preprocessor.rs b/crates/mdbook-goals/src/mdbook_preprocessor.rs index 5c2a7f3f..736a9e11 100644 --- a/crates/mdbook-goals/src/mdbook_preprocessor.rs +++ b/crates/mdbook-goals/src/mdbook_preprocessor.rs @@ -233,13 +233,16 @@ impl<'c> GoalPreprocessorWithContext<'c> { // Extract out the list of goals with the given status. let goals = self.goal_documents(chapter_path)?; - let mut goals_with_status: Vec<&GoalDocument> = - goals.iter().filter(|g| filter(g.metadata.status)).collect(); + let mut goals_with_status: Vec<&GoalDocument> = goals + .iter() + .filter(|g| filter(g.metadata.status.content)) + .collect(); goals_with_status.sort_by_key(|g| &g.metadata.title); // Format the list of goals and replace the `` comment with that. - let output = goal::format_goal_table(&goals_with_status)?; + let output = + goal::format_goal_table(&goals_with_status).map_err(|e| anyhow::anyhow!("{e}"))?; chapter.content.replace_range(range, &output); // Populate with children if this is not README @@ -281,7 +284,8 @@ impl<'c> GoalPreprocessorWithContext<'c> { .filter(|g| g.metadata.status.is_not_not_accepted()) .flat_map(|g| &g.team_asks) .collect(); - let format_team_asks = format_team_asks(&asks_of_any_team)?; + let format_team_asks = + format_team_asks(&asks_of_any_team).map_err(|e| anyhow::anyhow!("{e}"))?; chapter.content.replace_range(range, &format_team_asks); Ok(()) @@ -323,7 +327,8 @@ impl<'c> GoalPreprocessorWithContext<'c> { return Ok(goals.clone()); } - let goal_documents = goal::goals_in_dir(&self.ctx.config.book.src.join(milestone_path))?; + let goal_documents = goal::goals_in_dir(&self.ctx.config.book.src.join(milestone_path)) + .map_err(|e| anyhow::anyhow!("{e}"))?; let goals = Arc::new(goal_documents); self.goal_document_map .insert(milestone_path.to_path_buf(), goals.clone()); @@ -380,19 +385,21 @@ impl<'c> GoalPreprocessorWithContext<'c> { return Ok(n.clone()); } - let display_name = match team::get_person_data(username)? { - Some(person) => person.data.name.clone(), - None => match GithubUserInfo::load(username) - .with_context(|| format!("loading user info for {}", username)) - { - Ok(GithubUserInfo { name: Some(n), .. }) => n, - Ok(GithubUserInfo { name: None, .. }) => username.to_string(), - Err(e) => { - eprintln!("{:?}", e); - username.to_string() - } - }, - }; + let display_name = + match team::get_person_data(username).map_err(|e| anyhow::anyhow!("{e}"))? { + Some(person) => person.data.name.clone(), + None => match GithubUserInfo::load(username) + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("loading user info for {}", username)) + { + Ok(GithubUserInfo { name: Some(n), .. }) => n, + Ok(GithubUserInfo { name: None, .. }) => username.to_string(), + Err(e) => { + eprintln!("{:?}", e); + username.to_string() + } + }, + }; let display_name = Rc::new(display_name); self.display_names .insert(username.to_string(), display_name.clone()); @@ -421,7 +428,7 @@ impl<'c> GoalPreprocessorWithContext<'c> { fn link_teams(&self, chapter: &mut Chapter) -> anyhow::Result<()> { chapter.content.push_str("\n\n"); - for team in team::get_team_names()? { + for team in team::get_team_names().map_err(|e| anyhow::anyhow!("{e}"))? { chapter .content .push_str(&format!("{team}: {}\n", team.url())); diff --git a/crates/rust-project-goals-cli/src/cfp.rs b/crates/rust-project-goals-cli/src/cfp.rs index 0dd0a879..88758d05 100644 --- a/crates/rust-project-goals-cli/src/cfp.rs +++ b/crates/rust-project-goals-cli/src/cfp.rs @@ -1,5 +1,6 @@ -use anyhow::{Context, Result}; use regex::Regex; +use rust_project_goals::spanned::{Error, Spanned}; +use rust_project_goals::{spanned::Context as _, spanned::Result}; use std::fs::{self, File}; use std::io::Write; use std::path::{Path, PathBuf}; @@ -288,8 +289,9 @@ pub fn create_cfp(timeframe: &str, force: bool, dry_run: bool) -> Result<()> { println!("Would create/overwrite directory: {}", dir_path.display()); } } else if !dry_run { - fs::create_dir_all(&dir_path) - .with_context(|| format!("Failed to create directory {}", dir_path.display()))?; + fs::create_dir_all(&dir_path).with_context(|| { + Spanned::here(format!("Failed to create directory {}", dir_path.display())) + })?; println!("Created directory: {}", dir_path.display()); } else { println!("Would create directory: {}", dir_path.display()); @@ -342,7 +344,7 @@ pub fn create_cfp(timeframe: &str, force: bool, dry_run: bool) -> Result<()> { fn validate_timeframe(timeframe: &str) -> Result<()> { let re = Regex::new(r"^\d{4}[hH][12]$").unwrap(); if !re.is_match(timeframe) { - anyhow::bail!("Invalid timeframe format. Expected format: YYYYhN or YYYYHN (e.g., 2025h1, 2025H1, 2025h2, or 2025H2)"); + return Err(Error::str("Invalid timeframe format. Expected format: YYYYhN or YYYYHN (e.g., 2025h1, 2025H1, 2025h2, or 2025H2")); } Ok(()) } @@ -356,8 +358,7 @@ fn copy_and_process_template( dry_run: bool, ) -> Result<()> { // Read the template file - let template_content = fs::read_to_string(template_path) - .with_context(|| format!("Failed to read template file: {}", template_path))?; + let template_content = Spanned::read_str_from_file(template_path).transpose()?; // Use the pure function to process the content let processed_content = text_processing::process_template_content( @@ -369,9 +370,9 @@ fn copy_and_process_template( // Write to destination file if !dry_run { File::create(dest_path) - .with_context(|| format!("Failed to create file: {}", dest_path.display()))? + .with_path_context(dest_path, "Failed to create file")? .write_all(processed_content.as_bytes()) - .with_context(|| format!("Failed to write to file: {}", dest_path.display()))?; + .with_path_context(dest_path, "Failed to write to file")?; println!("Created file: {}", dest_path.display()); } else { @@ -383,8 +384,7 @@ fn copy_and_process_template( /// Updates the SUMMARY.md file to include the new timeframe section fn update_summary_md(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) -> Result<()> { let summary_path = "src/SUMMARY.md"; - let content = - fs::read_to_string(summary_path).with_context(|| format!("Failed to read SUMMARY.md"))?; + let content = fs::read_to_string(summary_path).with_str_context("Failed to read SUMMARY.md")?; // Use the pure function to process the content let new_content = @@ -408,8 +408,7 @@ fn update_summary_md(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) // Write the updated content back to SUMMARY.md if !dry_run { - fs::write(summary_path, new_content) - .with_context(|| format!("Failed to write to SUMMARY.md"))?; + fs::write(summary_path, new_content).with_str_context("Failed to write to SUMMARY.md")?; println!("Updated SUMMARY.md with {} section", timeframe); } else { @@ -422,8 +421,7 @@ fn update_summary_md(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) /// Updates the src/README.md with information about the new timeframe fn update_main_readme(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) -> Result<()> { let readme_path = "src/README.md"; - let content = - fs::read_to_string(readme_path).with_context(|| format!("Failed to read README.md"))?; + let content = fs::read_to_string(readme_path).with_str_context("Failed to read README.md")?; // Use the pure function to process the content let new_content = @@ -495,8 +493,7 @@ fn update_main_readme(timeframe: &str, lowercase_timeframe: &str, dry_run: bool) // Write the updated content back to README.md if !dry_run { - fs::write(readme_path, new_content) - .with_context(|| format!("Failed to write to src/README.md"))?; + fs::write(readme_path, new_content).with_str_context("Failed to write to src/README.md")?; } Ok(()) diff --git a/crates/rust-project-goals-cli/src/generate_json.rs b/crates/rust-project-goals-cli/src/generate_json.rs index f3275a0e..12abb63e 100644 --- a/crates/rust-project-goals-cli/src/generate_json.rs +++ b/crates/rust-project-goals-cli/src/generate_json.rs @@ -4,13 +4,14 @@ use rust_project_goals::gh::{ issue_id::Repository, issues::{checkboxes, list_issues_in_milestone, ExistingGithubComment}, }; +use rust_project_goals::spanned::Result; use rust_project_goals_json::{TrackingIssue, TrackingIssueUpdate, TrackingIssues}; pub(super) fn generate_json( repository: &Repository, milestone: &str, json_path: &Option, -) -> anyhow::Result<()> { +) -> Result<()> { let issues = list_issues_in_milestone(repository, milestone)?; let issues = TrackingIssues { diff --git a/crates/rust-project-goals-cli/src/main.rs b/crates/rust-project-goals-cli/src/main.rs index ebc3a601..1489ffe5 100644 --- a/crates/rust-project-goals-cli/src/main.rs +++ b/crates/rust-project-goals-cli/src/main.rs @@ -1,7 +1,9 @@ -use anyhow::Context; use clap::Parser; use regex::Regex; -use rust_project_goals::gh::issue_id::Repository; +use rust_project_goals::{ + gh::issue_id::Repository, + spanned::{Result, Spanned}, +}; use std::path::PathBuf; use walkdir::WalkDir; @@ -109,7 +111,7 @@ enum Command { }, } -fn main() -> anyhow::Result<()> { +fn main() -> Result<()> { let opt: Opt = Opt::parse(); match &opt.cmd { @@ -138,8 +140,11 @@ fn main() -> anyhow::Result<()> { commit, sleep, } => { - rfc::generate_issues(&opt.repository, path, *commit, *sleep) - .with_context(|| format!("failed to adjust issues; rerun command to resume"))?; + rfc::generate_issues(&opt.repository, path, *commit, *sleep).map_err(|e| { + e.wrap_str(Spanned::here( + "failed to adjust issues; rerun command to resume", + )) + })?; } Command::TeamRepo { @@ -174,7 +179,7 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn check() -> anyhow::Result<()> { +fn check() -> Result<()> { // Look for all directories like `2024h2` or `2025h1` and load goals from those directories. let regex = Regex::new(r"\d\d\d\dh[12]")?; diff --git a/crates/rust-project-goals-cli/src/rfc.rs b/crates/rust-project-goals-cli/src/rfc.rs index 1894cc56..72cb6cdc 100644 --- a/crates/rust-project-goals-cli/src/rfc.rs +++ b/crates/rust-project-goals-cli/src/rfc.rs @@ -6,7 +6,6 @@ use std::{ time::Duration, }; -use anyhow::Context; use regex::Regex; use rust_project_goals::{ @@ -20,18 +19,17 @@ use rust_project_goals::{ labels::GhLabel, }, goal::{self, GoalDocument, GoalPlan, ParsedOwners}, + spanned::{self, Context, Error, Result}, team::{get_person_data, TeamName}, }; -fn validate_path(path: &Path) -> anyhow::Result { +fn validate_path(path: &Path) -> Result { if !path.is_dir() { - return Err(anyhow::anyhow!( - "RFC path should be a directory like src/2024h2" - )); + spanned::bail_here!("RFC path should be a directory like src/2024h2"); }; if path.is_absolute() { - return Err(anyhow::anyhow!("RFC path should be relative")); + spanned::bail_here!("RFC path should be relative"); } let timeframe = path @@ -40,12 +38,12 @@ fn validate_path(path: &Path) -> anyhow::Result { .unwrap() .as_os_str() .to_str() - .ok_or_else(|| anyhow::anyhow!("invalid path `{}`", path.display()))?; + .ok_or_else(|| Error::str(format!("invalid path `{}`", path.display())))?; Ok(timeframe.to_string()) } -pub fn generate_comment(path: &Path) -> anyhow::Result<()> { +pub fn generate_comment(path: &Path) -> Result<()> { let _ = validate_path(path)?; let goal_documents = goal::goals_in_dir(path)?; let teams_with_asks = teams_with_asks(&goal_documents); @@ -69,7 +67,7 @@ pub fn generate_comment(path: &Path) -> anyhow::Result<()> { Ok(()) } -pub fn generate_rfc(path: &Path) -> anyhow::Result<()> { +pub fn generate_rfc(path: &Path) -> Result<()> { let timeframe = &validate_path(path)?; // run mdbook build @@ -80,18 +78,11 @@ pub fn generate_rfc(path: &Path) -> anyhow::Result<()> { .join(timeframe) .join("index.md"); if !generated_path.exists() { - return Err(anyhow::anyhow!( - "no markdown generated at {}", - generated_path.display() - )); + spanned::bail_here!("no markdown generated at {}", generated_path.display()); } - let generated_text = std::fs::read_to_string(&generated_path).with_context(|| { - format!( - "reading generated markdown from `{}`", - generated_path.display() - ) - })?; + let generated_text = std::fs::read_to_string(&generated_path) + .with_path_context(&generated_path, "reading generated markdown")?; let regex = Regex::new(r"\]\(([^(]*)\.md(#[^)]*)?\)").unwrap(); @@ -110,13 +101,13 @@ pub fn generate_issues( path: &Path, commit: bool, sleep: u64, -) -> anyhow::Result<()> { +) -> Result<()> { // Verify the `gh` client is installed to compute which actions need to be taken in the repo. let sanity_check = Command::new("gh").arg("--version").output(); if sanity_check.is_err() { - return Err(anyhow::anyhow!( + spanned::bail_here!( "The github `gh` client is missing and needs to be installed and configured with a token." - )); + ); } // Hacky but works: we loop because after creating the issue, we sometimes have additional sync to do, @@ -166,7 +157,7 @@ pub fn generate_issues( } progress_bar::finalize_progress_bar(); if success == 0 { - anyhow::bail!("all actions failed, aborting") + spanned::bail_here!("all actions failed, aborting") } } else { eprintln!("Actions to be executed:"); @@ -242,7 +233,7 @@ enum GithubAction<'doc> { fn initialize_labels( repository: &Repository, teams_with_asks: &BTreeSet<&TeamName>, -) -> anyhow::Result>> { +) -> Result>> { const TEAM_LABEL_COLOR: &str = "bfd4f2"; let mut desired_labels: BTreeSet<_> = teams_with_asks @@ -283,12 +274,12 @@ fn initialize_issues<'doc>( repository: &Repository, timeframe: &str, goal_documents: &'doc [GoalDocument], -) -> anyhow::Result>> { +) -> Result>> { // the set of issues we want to exist let desired_issues: BTreeSet = goal_documents .iter() .map(|goal_document| issue(timeframe, goal_document)) - .collect::>()?; + .collect::>()?; // the list of existing issues in the target milestone let milestone_issues = list_issues_in_milestone(repository, timeframe)?; @@ -412,7 +403,7 @@ fn initialize_issues<'doc>( Ok(actions) } -fn issue<'doc>(timeframe: &str, document: &'doc GoalDocument) -> anyhow::Result> { +fn issue<'doc>(timeframe: &str, document: &'doc GoalDocument) -> Result> { let mut assignees = BTreeSet::default(); for username in document.metadata.owner_usernames() { if let Some(data) = get_person_data(username)? { @@ -443,7 +434,7 @@ fn goal_document_link(timeframe: &str, document: &GoalDocument) -> String { format!("[{timeframe}/{goal_file}](https://rust-lang.github.io/rust-project-goals/{timeframe}/{goal_file}.html)") } -fn issue_text(timeframe: &str, document: &GoalDocument) -> anyhow::Result { +fn issue_text(timeframe: &str, document: &GoalDocument) -> Result { let mut tasks = vec![]; for goal_plan in &document.goal_plans { tasks.extend(task_items(goal_plan)?); @@ -481,7 +472,7 @@ fn issue_text(timeframe: &str, document: &GoalDocument) -> anyhow::Result anyhow::Result> { +fn task_items(goal_plan: &GoalPlan) -> Result> { use std::fmt::Write; let mut tasks = vec![]; @@ -494,7 +485,7 @@ fn task_items(goal_plan: &GoalPlan) -> anyhow::Result> { let mut description = format!( "* {box} {text}", box = if plan_item.is_complete() { "[x]" } else { "[ ]" }, - text = plan_item.text + text = plan_item.text.content ); if let Some(parsed_owners) = plan_item.parse_owners()? { @@ -584,7 +575,7 @@ impl Display for GithubAction<'_> { } impl GithubAction<'_> { - pub fn execute(self, repository: &Repository, timeframe: &str) -> anyhow::Result<()> { + pub fn execute(self, repository: &Repository, timeframe: &str) -> Result<()> { match self { GithubAction::CreateLabel { label } => { label.create(repository)?; diff --git a/crates/rust-project-goals-cli/src/team_repo.rs b/crates/rust-project-goals-cli/src/team_repo.rs index 17618daa..f50b1b79 100644 --- a/crates/rust-project-goals-cli/src/team_repo.rs +++ b/crates/rust-project-goals-cli/src/team_repo.rs @@ -2,16 +2,18 @@ use std::collections::BTreeSet; use std::fmt::Write; use std::process::Command; -use anyhow::Context; - -use rust_project_goals::{goal, team}; +use rust_project_goals::{ + goal, + spanned::{self, Context as _, Result}, + team, +}; pub(crate) fn generate_team_repo( paths: &[std::path::PathBuf], team_repo_path: &std::path::PathBuf, -) -> anyhow::Result<()> { +) -> Result<()> { if !team_repo_path.is_dir() { - anyhow::bail!( + spanned::bail_here!( "output path not a directory: `{}`", team_repo_path.display() ); @@ -37,7 +39,7 @@ pub(crate) fn generate_team_repo( let team_file = team_file(&owners)?; let team_toml_file = team_repo_path.join("teams").join("goal-owners.toml"); std::fs::write(&team_toml_file, team_file) - .with_context(|| format!("writing to `{}`", team_toml_file.display()))?; + .with_path_context(&team_toml_file, "writing team toml file")?; progress_bar::inc_progress_bar(); // generate rudimentary people files if needed @@ -56,7 +58,7 @@ pub(crate) fn generate_team_repo( Ok(()) } -fn ensure_person_file(owner: &str, team_repo_path: &std::path::PathBuf) -> anyhow::Result<()> { +fn ensure_person_file(owner: &str, team_repo_path: &std::path::PathBuf) -> Result<()> { let person_toml_file = team_repo_path .join("people") .join(&owner[1..]) @@ -78,16 +80,16 @@ fn ensure_person_file(owner: &str, team_repo_path: &std::path::PathBuf) -> anyho .arg(&owner[1..]) .current_dir(team_repo_path) .status() - .with_context(|| format!("running `cargo run add-person` for {owner}"))?; + .with_str_context(format!("running `cargo run add-person` for {owner}"))?; if !status.success() { - anyhow::bail!("`cargo run add-person` failed for {owner}"); + spanned::bail_here!("`cargo run add-person` failed for {owner}"); } Ok(()) } -fn team_file(owners: &BTreeSet<&str>) -> anyhow::Result { +fn team_file(owners: &BTreeSet<&str>) -> Result { let mut out = String::new(); writeln!( out, diff --git a/crates/rust-project-goals-cli/src/updates.rs b/crates/rust-project-goals-cli/src/updates.rs index 74e893d5..cb75e930 100644 --- a/crates/rust-project-goals-cli/src/updates.rs +++ b/crates/rust-project-goals-cli/src/updates.rs @@ -1,10 +1,9 @@ -use anyhow::Context; use chrono::{Datelike, NaiveDate}; use regex::Regex; -use rust_project_goals::markwaydown; use rust_project_goals::re::{HELP_WANTED, TLDR}; -use rust_project_goals::spanned::{Span, Spanned}; +use rust_project_goals::spanned::{Context as _, Result, Span, Spanned}; use rust_project_goals::util::comma; +use rust_project_goals::{markwaydown, spanned}; use rust_project_goals_json::GithubIssueState; use std::io::Write; use std::path::Path; @@ -25,14 +24,14 @@ pub(crate) fn generate_updates( start_date: &Option, end_date: &Option, vscode: bool, -) -> anyhow::Result<()> { +) -> Result<()> { if output_file.is_none() && !vscode { - anyhow::bail!("either `--output-file` or `--vscode` must be specified"); + spanned::bail_here!("either `--output-file` or `--vscode` must be specified"); } let milestone_re = Regex::new(r"^\d{4}[hH][12]$").unwrap(); if !milestone_re.is_match(milestone) { - anyhow::bail!( + spanned::bail_here!( "the milestone `{}` does not follow the `$year$semester` format, where $semester is `h1` or `h2`", milestone, ); @@ -65,24 +64,23 @@ pub(crate) fn generate_updates( let output = updates.render()?; if let Some(output_file) = output_file { - std::fs::write(&output_file, output) - .with_context(|| format!("failed to write to `{}`", output_file.display()))?; + std::fs::write(&output_file, output).with_path_context(output_file, "failed to write")?; } else if vscode { let mut child = Command::new("code") .arg("-") .stdin(Stdio::piped()) .spawn() - .with_context(|| "failed to spawn `code` process")?; + .with_str_context("failed to spawn `code` process")?; if let Some(stdin) = child.stdin.as_mut() { stdin .write_all(output.as_bytes()) - .with_context(|| "failed to write to `code` stdin")?; + .with_str_context("failed to write to `code` stdin")?; } child .wait() - .with_context(|| "failed to wait on `code` process")?; + .with_str_context("failed to wait on `code` process")?; } else { println!("{output}"); } @@ -95,7 +93,7 @@ fn prepare_goals( issues: &[ExistingGithubIssue], filter: &Filter<'_>, flagship: bool, -) -> anyhow::Result> { +) -> Result> { let mut result = vec![]; // We process flagship and regular goals in two passes, and capture comments differently for flagship goals. for issue in issues { @@ -169,10 +167,7 @@ fn prepare_goals( } /// Search for a TL;DR comment. If one is found, remove it and return the text. -fn tldr( - _issue_id: &IssueId, - comments: &mut Vec, -) -> anyhow::Result> { +fn tldr(_issue_id: &IssueId, comments: &mut Vec) -> Result> { // `comments` are sorted by creation date in an ascending order, so we look for the most recent // TL;DR comment from the end. let Some(index) = comments.iter().rposition(|c| c.body.starts_with(TLDR)) else { @@ -188,7 +183,7 @@ fn help_wanted( _issue_id: &IssueId, tldr: &Option, comments: &[ExistingGithubComment], -) -> anyhow::Result<(bool, Vec)> { +) -> Result<(bool, Vec)> { use std::fmt::Write; let mut help_wanted = vec![]; @@ -229,7 +224,7 @@ fn help_wanted( Ok((tldr_has_help_wanted || !help_wanted.is_empty(), help_wanted)) } -fn why_this_goal(issue_id: &IssueId, issue: &ExistingGithubIssue) -> anyhow::Result { +fn why_this_goal(issue_id: &IssueId, issue: &ExistingGithubIssue) -> Result { let span = Span { file: issue_id.url().into(), bytes: 0..issue.body.len(), diff --git a/crates/rust-project-goals-cli/src/updates/templates.rs b/crates/rust-project-goals-cli/src/updates/templates.rs index 6e8b8e15..973b369a 100644 --- a/crates/rust-project-goals-cli/src/updates/templates.rs +++ b/crates/rust-project-goals-cli/src/updates/templates.rs @@ -4,6 +4,7 @@ use handlebars::{DirectorySourceOptions, Handlebars}; use rust_project_goals::gh::issues::ExistingGithubComment; use serde::Serialize; +use rust_project_goals::spanned::Result; use rust_project_goals_json::Progress; pub struct Templates<'h> { @@ -11,12 +12,12 @@ pub struct Templates<'h> { } impl<'h> Templates<'h> { - pub fn new() -> anyhow::Result { + pub fn new() -> Result { let templates = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../templates"); Self::from_templates_dir(&templates) } - pub fn from_templates_dir(dir_path: impl AsRef) -> anyhow::Result { + pub fn from_templates_dir(dir_path: impl AsRef) -> Result { let dir_path = dir_path.as_ref(); let mut reg = Handlebars::new(); @@ -64,7 +65,7 @@ impl Updates { other_goals, } } - pub fn render(self) -> anyhow::Result { + pub fn render(self) -> Result { let templates = Templates::new()?; Ok(templates.reg.render("updates", &self)?) } diff --git a/crates/rust-project-goals/Cargo.toml b/crates/rust-project-goals/Cargo.toml index ad043859..5abe27cc 100644 --- a/crates/rust-project-goals/Cargo.toml +++ b/crates/rust-project-goals/Cargo.toml @@ -16,4 +16,4 @@ rust_team_data = { git = "https://github.com/rust-lang/team" } rust-project-goals-json = { version = "0.1.0", path = "../rust-project-goals-json" } toml = "0.8.19" indexmap = "2.7.1" -spanned = "0.5.0" +spanned = "0.6.1" diff --git a/crates/rust-project-goals/src/config.rs b/crates/rust-project-goals/src/config.rs index 1c3bda92..b138e44e 100644 --- a/crates/rust-project-goals/src/config.rs +++ b/crates/rust-project-goals/src/config.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; -use anyhow::Context; use indexmap::IndexMap; use serde::Deserialize; +use spanned::{Context as _, Result}; #[derive(Deserialize)] pub struct Configuration { @@ -28,11 +28,11 @@ impl Configuration { &*CONFIG } - fn load() -> anyhow::Result { + fn load() -> Result { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let toml_file = manifest_dir.join("../../rust-project-goals.toml"); let toml_string = std::fs::read_to_string(&toml_file) - .with_context(|| format!("loading configuration from {}", toml_file.display()))?; + .with_path_context(&toml_file, "loading configuration")?; Ok(toml::from_str(&toml_string)?) } } diff --git a/crates/rust-project-goals/src/format_team_ask.rs b/crates/rust-project-goals/src/format_team_ask.rs index f58072cd..22dcdf69 100644 --- a/crates/rust-project-goals/src/format_team_ask.rs +++ b/crates/rust-project-goals/src/format_team_ask.rs @@ -3,7 +3,7 @@ use std::{ path::PathBuf, }; -use spanned::Spanned; +use spanned::{Result, Spanned}; use crate::{ config::Configuration, @@ -24,7 +24,7 @@ use crate::{ /// /// \*1: ... longer notes that would not fit ... /// ``` -pub fn format_team_asks(asks_of_any_team: &[&TeamAsk]) -> anyhow::Result { +pub fn format_team_asks(asks_of_any_team: &[&TeamAsk]) -> Result { use std::fmt::Write; const CHECK: &str = "✅"; @@ -156,7 +156,7 @@ struct GoalData<'g> { } impl<'g> GoalData<'g> { - fn new(ask: &'g TeamAsk) -> anyhow::Result { + fn new(ask: &'g TeamAsk) -> Result { match &ask.goal_titles[..] { [goal_title] => Ok(Self { goal_title, @@ -168,9 +168,10 @@ impl<'g> GoalData<'g> { subgoal_title: Some(subgoal_title), link: &ask.link_path, }), - _ => anyhow::bail!( - "expected either 1 or 2 goal titles, not {:?}", - ask.goal_titles + _ => spanned::bail!( + ask.goal_titles[3], + "expected either 1 or 2 goal titles, not {}", + ask.goal_titles.len(), ), } } diff --git a/crates/rust-project-goals/src/gh/issues.rs b/crates/rust-project-goals/src/gh/issues.rs index 54d540d9..6edb088b 100644 --- a/crates/rust-project-goals/src/gh/issues.rs +++ b/crates/rust-project-goals/src/gh/issues.rs @@ -1,9 +1,9 @@ use std::{collections::BTreeSet, process::Command, str::FromStr}; -use anyhow::Context; use chrono::NaiveDate; use rust_project_goals_json::{GithubIssueState, Progress}; use serde::{Deserialize, Serialize}; +use spanned::{Context, Error, Result}; use crate::{re, util::comma}; @@ -68,10 +68,7 @@ pub struct CountIssues { pub closed: u32, } -pub fn count_issues_matching_search( - repository: &Repository, - search: &str, -) -> anyhow::Result { +pub fn count_issues_matching_search(repository: &Repository, search: &str) -> Result { #[derive(Deserialize)] struct JustState { state: GithubIssueState, @@ -104,7 +101,7 @@ pub fn count_issues_matching_search( Ok(count_issues) } -pub fn fetch_issue(repository: &Repository, issue: u64) -> anyhow::Result { +pub fn fetch_issue(repository: &Repository, issue: u64) -> Result { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -123,14 +120,14 @@ pub fn fetch_issue(repository: &Repository, issue: u64) -> anyhow::Result anyhow::Result> { +) -> Result> { list_issues(repository, &[("-m", timeframe)]) } pub fn list_issues( repository: &Repository, filter: &[(&str, &str)], -) -> anyhow::Result> { +) -> Result> { let mut cmd = Command::new("gh"); cmd.arg("-R") @@ -151,7 +148,7 @@ pub fn list_issues( .arg("--json") .arg("title,assignees,number,comments,body,state,labels,milestone") .output() - .with_context(|| format!("running github cli tool `gh`"))?; + .with_str_context("running github cli tool `gh`")?; let existing_issues: Vec = serde_json::from_slice(&output.stdout)?; @@ -168,7 +165,7 @@ pub fn create_issue( labels: &[String], assignees: &BTreeSet, milestone: &str, -) -> anyhow::Result<()> { +) -> Result<()> { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -187,17 +184,17 @@ pub fn create_issue( .output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to create issue `{}`: {}", title, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } } -pub fn change_title(repository: &Repository, number: u64, title: &str) -> anyhow::Result<()> { +pub fn change_title(repository: &Repository, number: u64, title: &str) -> Result<()> { let mut command = Command::new("gh"); command .arg("-R") @@ -210,21 +207,17 @@ pub fn change_title(repository: &Repository, number: u64, title: &str) -> anyhow let output = command.output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to change milestone `{}`: {}", number, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } } -pub fn change_milestone( - repository: &Repository, - number: u64, - milestone: &str, -) -> anyhow::Result<()> { +pub fn change_milestone(repository: &Repository, number: u64, milestone: &str) -> Result<()> { let mut command = Command::new("gh"); command .arg("-R") @@ -237,17 +230,17 @@ pub fn change_milestone( let output = command.output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to change milestone `{}`: {}", number, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } } -pub fn create_comment(repository: &Repository, number: u64, body: &str) -> anyhow::Result<()> { +pub fn create_comment(repository: &Repository, number: u64, body: &str) -> Result<()> { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -259,17 +252,17 @@ pub fn create_comment(repository: &Repository, number: u64, body: &str) -> anyho .output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to leave comment on issue `{}`: {}", number, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } } -pub fn update_issue_body(repository: &Repository, number: u64, body: &str) -> anyhow::Result<()> { +pub fn update_issue_body(repository: &Repository, number: u64, body: &str) -> Result<()> { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -281,11 +274,11 @@ pub fn update_issue_body(repository: &Repository, number: u64, body: &str) -> an .output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to adjust issue body on issue `{}`: {}", number, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } @@ -296,7 +289,7 @@ pub fn sync_assignees( number: u64, remove_owners: &BTreeSet, add_owners: &BTreeSet, -) -> anyhow::Result<()> { +) -> Result<()> { let mut command = Command::new("gh"); command .arg("-R") @@ -315,11 +308,11 @@ pub fn sync_assignees( let output = command.output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to sync issue `{}`: {}", number, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } @@ -349,7 +342,7 @@ impl ExistingGithubIssue { } } -pub fn lock_issue(repository: &Repository, number: u64) -> anyhow::Result<()> { +pub fn lock_issue(repository: &Repository, number: u64) -> Result<()> { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -360,11 +353,11 @@ pub fn lock_issue(repository: &Repository, number: u64) -> anyhow::Result<()> { if !output.status.success() { if !output.stderr.starts_with(b"already locked") { - return Err(anyhow::anyhow!( + return Err(Error::str(format!( "failed to lock issue `{}`: {}", number, String::from_utf8_lossy(&output.stderr) - )); + ))); } } @@ -424,14 +417,14 @@ pub fn checkboxes(issue: &ExistingGithubIssue) -> Progress { } } -fn try_checkboxes(issue: &ExistingGithubIssue) -> anyhow::Result { +fn try_checkboxes(issue: &ExistingGithubIssue) -> Result { let mut completed = 0; let mut total = 0; for line in issue.body.lines() { // Does this match TRACKED_ISSUES? if let Some(c) = re::TRACKED_ISSUES_QUERY.captures(line) { - let repo = Repository::from_str(&c["repo"])?; + let repo = Repository::from_str(&c["repo"]).map_err(|e| Error::str(e.to_string()))?; let query = &c["query"]; let CountIssues { open, closed } = count_issues_matching_search(&repo, query)?; @@ -450,7 +443,9 @@ fn try_checkboxes(issue: &ExistingGithubIssue) -> anyhow::Result { ) { (Some(c), _) => c, (None, Some(c)) => c, - (None, None) => anyhow::bail!("invalid issue URL `{issue_url}`"), + (None, None) => { + spanned::bail_here!("invalid issue URL `{issue_url}`") + } }; let repository = Repository::new(&c["org"], &c["repo"]); let issue_number = c["issue"].parse::()?; @@ -472,7 +467,7 @@ fn try_checkboxes(issue: &ExistingGithubIssue) -> anyhow::Result { } Progress::Error { message } => { - anyhow::bail!("error parsing {repository}#{issue_number}: {message}") + spanned::bail_here!("error parsing {repository}#{issue_number}: {message}") } } } diff --git a/crates/rust-project-goals/src/gh/labels.rs b/crates/rust-project-goals/src/gh/labels.rs index f96b6cc6..a6c1de0d 100644 --- a/crates/rust-project-goals/src/gh/labels.rs +++ b/crates/rust-project-goals/src/gh/labels.rs @@ -3,6 +3,7 @@ use std::process::Command; use serde::{Deserialize, Serialize}; use super::issue_id::Repository; +use spanned::{Error, Result}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct GhLabel { @@ -11,7 +12,7 @@ pub struct GhLabel { } impl GhLabel { - pub fn list(repository: &Repository) -> anyhow::Result> { + pub fn list(repository: &Repository) -> Result> { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -26,7 +27,7 @@ impl GhLabel { Ok(labels) } - pub fn create(&self, repository: &Repository) -> anyhow::Result<()> { + pub fn create(&self, repository: &Repository) -> Result<()> { let output = Command::new("gh") .arg("-R") .arg(&repository.to_string()) @@ -39,11 +40,11 @@ impl GhLabel { .output()?; if !output.status.success() { - Err(anyhow::anyhow!( + Err(Error::str(format!( "failed to create label `{}`: {}", self.name, String::from_utf8_lossy(&output.stderr) - )) + ))) } else { Ok(()) } diff --git a/crates/rust-project-goals/src/goal.rs b/crates/rust-project-goals/src/goal.rs index 2b865a46..e9b0decc 100644 --- a/crates/rust-project-goals/src/goal.rs +++ b/crates/rust-project-goals/src/goal.rs @@ -2,9 +2,8 @@ use std::path::Path; use std::sync::Arc; use std::{collections::BTreeSet, path::PathBuf}; -use anyhow::{bail, Context}; use regex::Regex; -use spanned::Spanned; +use spanned::{Error, Result, Spanned}; use crate::config::{Configuration, TeamAskDetails}; use crate::gh::issue_id::{IssueId, Repository}; @@ -46,7 +45,7 @@ pub struct Metadata { pub title: String, pub short_title: Spanned, pub pocs: String, - pub status: Status, + pub status: Spanned, pub tracking_issue: Option, pub table: Spanned, } @@ -66,7 +65,7 @@ pub struct GoalPlan { /// Identifies a particular ask for a set of Rust teams #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct PlanItem { - pub text: String, + pub text: Spanned, pub owners: String, pub notes: String, } @@ -104,12 +103,10 @@ pub struct TeamAsk { } /// Load all the goals from a given directory -pub fn goals_in_dir(directory_path: &Path) -> anyhow::Result> { +pub fn goals_in_dir(directory_path: &Path) -> Result> { let mut goal_documents = vec![]; for (path, link_path) in markdown_files(&directory_path)? { - if let Some(goal_document) = GoalDocument::load(&path, &link_path) - .with_context(|| format!("loading goal from `{}`", path.display()))? - { + if let Some(goal_document) = GoalDocument::load(&path, &link_path)? { goal_documents.push(goal_document); } } @@ -117,7 +114,7 @@ pub fn goals_in_dir(directory_path: &Path) -> anyhow::Result> } impl GoalDocument { - fn load(path: &Path, link_path: &Path) -> anyhow::Result> { + fn load(path: &Path, link_path: &Path) -> Result> { let sections = markwaydown::parse(path)?; let Some(metadata) = extract_metadata(§ions)? else { @@ -147,7 +144,7 @@ impl GoalDocument { // Enforce that every goal has some team asks (unless it is not accepted) if metadata.status.is_not_not_accepted() && team_asks.is_empty() { - anyhow::bail!("no team asks in goal; did you include `![Team]` in the table?"); + spanned::bail_here!("no team asks in goal; did you include `![Team]` in the table?"); } let task_owners = goal_plans @@ -181,7 +178,7 @@ impl GoalDocument { } /// Modify the goal document on disk to link to the given issue number in the metadata. - pub fn link_issue(&self, number: IssueId) -> anyhow::Result<()> { + pub fn link_issue(&self, number: IssueId) -> Result<()> { let mut metadata_table = self.metadata.table.clone(); metadata_table .content @@ -202,7 +199,7 @@ impl GoalDocument { } } -pub fn format_goal_table(goals: &[&GoalDocument]) -> anyhow::Result { +pub fn format_goal_table(goals: &[&GoalDocument]) -> Result { // If any of the goals have tracking issues, include those in the table. let goals_are_proposed = goals .iter() @@ -292,12 +289,8 @@ impl Status { pub fn is_not_not_accepted(&self) -> bool { self.acceptance != AcceptanceStatus::NotAccepted } -} - -impl TryFrom<&str> for Status { - type Error = anyhow::Error; - fn try_from(value: &str) -> anyhow::Result { + pub fn try_from(value: Spanned<&str>) -> Result> { let value = value.trim(); let valid_values = [ @@ -361,13 +354,13 @@ impl TryFrom<&str> for Status { for (valid_value, status) in valid_values { if value == valid_value { - return Ok(status); + return Ok(value.map(|_| status)); } } - anyhow::bail!( - "unrecognized status `{}`, expected one of {:?}", + spanned::bail!( value, + "unrecognized status, expected one of {:?}", valid_values.iter().map(|(s, _)| s).collect::>(), ) } @@ -380,13 +373,13 @@ pub enum AcceptanceStatus { NotAccepted, } -fn extract_metadata(sections: &[Section]) -> anyhow::Result> { +fn extract_metadata(sections: &[Section]) -> Result> { let Some(first_section) = sections.first() else { - anyhow::bail!("no markdown sections found in input") + spanned::bail_here!("no markdown sections found in input") }; if first_section.title.is_empty() { - anyhow::bail!("first section has no title"); + spanned::bail!(first_section.title, "first section has no title"); } let title = &first_section.title; @@ -404,21 +397,24 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { .iter() .find(|row| row[0] == "Point of contact") else { - anyhow::bail!("metadata table has no `Point of contact` row") + spanned::bail!( + first_table.rows[0][0], + "metadata table has no `Point of contact` row" + ) }; if !re::is_just(&re::USERNAME, poc_row[1].trim()) { - anyhow::bail!( - "point of contact must be a single github username (found {:?})", - poc_row[1] + spanned::bail!( + poc_row[1], + "point of contact must be a single github username", ) } let Some(status_row) = first_table.rows.iter().find(|row| row[0] == "Status") else { - anyhow::bail!("metadata table has no `Status` row") + spanned::bail!(first_table.rows[0][0], "metadata table has no `Status` row") }; - let status = Status::try_from(status_row[1].as_str())?; + let status = Status::try_from(status_row[1].as_deref())?; let issue = if let Some(r) = first_table .rows @@ -427,16 +423,13 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { { // Accepted goals must have a tracking issue. let has_tracking_issue = !r[1].is_empty(); - if status.acceptance == AcceptanceStatus::Accepted { - anyhow::ensure!( - has_tracking_issue, - "accepted goals cannot have an empty tracking issue" - ); + if status.acceptance == AcceptanceStatus::Accepted && !has_tracking_issue { + spanned::bail!(r[1], "accepted goals cannot have an empty tracking issue"); } // For the others, it's of course optional. if has_tracking_issue { - Some(r[1].parse().transpose()?.content) + Some(r[1].parse()?.content) } else { None } @@ -461,13 +454,14 @@ fn extract_metadata(sections: &[Section]) -> anyhow::Result> { })) } -fn verify_row(rows: &[Vec>], key: &str, value: &str) -> anyhow::Result<()> { +fn verify_row(rows: &[Vec>], key: &str, value: &str) -> Result<()> { let Some(row) = rows.iter().find(|row| row[0] == key) else { - anyhow::bail!("metadata table has no `{}` row", key) + spanned::bail!(rows[0][0], "metadata table has no `{}` row", key) }; if row[1] != value { - anyhow::bail!( + spanned::bail!( + row[1], "metadata table has incorrect `{}` row, expected `{}`", key, value @@ -477,7 +471,7 @@ fn verify_row(rows: &[Vec>], key: &str, value: &str) -> anyhow:: Ok(()) } -fn extract_summary(sections: &[Section]) -> anyhow::Result> { +fn extract_summary(sections: &[Section]) -> Result> { let Some(ownership_section) = sections.iter().find(|section| section.title == "Summary") else { return Ok(None); }; @@ -485,12 +479,12 @@ fn extract_summary(sections: &[Section]) -> anyhow::Result> { Ok(Some(ownership_section.text.trim().to_string())) } -fn extract_plan_items<'i>(sections: &[Section]) -> anyhow::Result> { +fn extract_plan_items<'i>(sections: &[Section]) -> Result> { let Some(ownership_index) = sections .iter() .position(|section| section.title == "Ownership and team asks") else { - anyhow::bail!("no `Ownership and team asks` section found") + spanned::bail_here!("no `Ownership and team asks` section found") }; // Extract the plan items from the main section (if any) @@ -508,7 +502,8 @@ fn extract_plan_items<'i>(sections: &[Section]) -> anyhow::Result> } if goal_plans.is_empty() { - anyhow::bail!( + spanned::bail!( + sections[ownership_index].title, "no goal table items found in the `Ownership and team asks` section or subsections" ) } @@ -516,10 +511,7 @@ fn extract_plan_items<'i>(sections: &[Section]) -> anyhow::Result> Ok(goal_plans) } -fn goal_plan( - subgoal: Option>, - section: &Section, -) -> anyhow::Result> { +fn goal_plan(subgoal: Option>, section: &Section) -> Result> { match section.tables.len() { 0 => Ok(None), 1 => { @@ -537,22 +529,23 @@ fn goal_plan( plan_items, })) } - _ => anyhow::bail!( + _ => spanned::bail!( + section.title, "multiple goal tables found in section `{:?}`", - section.title + section.title.content, ), } } fn extract_plan_item( rows: &mut std::iter::Peekable>>>, -) -> anyhow::Result { +) -> Result { let Some(row) = rows.next() else { - anyhow::bail!("unexpected end of table"); + spanned::bail_here!("unexpected end of table"); }; Ok(PlanItem { - text: row[0].to_string(), + text: row[0].clone(), owners: row[1].to_string(), notes: row[2].to_string(), }) @@ -560,7 +553,7 @@ fn extract_plan_item( impl PlanItem { /// Parses the owners of this plan item. - pub fn parse_owners(&self) -> anyhow::Result> { + pub fn parse_owners(&self) -> Result> { if self.owners.is_empty() { Ok(None) } else if self.is_team_ask() { @@ -587,7 +580,7 @@ impl PlanItem { } /// Return the set of teams being asked to do things by this item, or empty vector if this is not a team ask. - pub fn teams_being_asked(&self) -> anyhow::Result> { + pub fn teams_being_asked(&self) -> Result> { if !self.is_team_ask() { return Ok(vec![]); } @@ -595,10 +588,12 @@ impl PlanItem { let mut teams = vec![]; for team_name in extract_team_names(&self.owners) { let Some(team) = team::get_team_name(&team_name)? else { - anyhow::bail!( + let names = team::get_team_names()?; + spanned::bail!( + self.text, "no Rust team named `{}` found (valid names are {})", team_name, - commas(team::get_team_names()?), + commas(names), ); }; @@ -606,7 +601,11 @@ impl PlanItem { } if teams.is_empty() { - anyhow::bail!("team ask for \"{}\" does not list any teams", self.text); + spanned::bail!( + self.text, + "team ask for \"{}\" does not list any teams", + self.text.content + ); } Ok(teams) @@ -639,30 +638,33 @@ impl PlanItem { link_path: &Arc, goal_titles: &Vec>, goal_owners: &str, - ) -> anyhow::Result> { + ) -> Result> { let mut asks = vec![]; let teams = self.teams_being_asked()?; if !teams.is_empty() { let config = Configuration::get(); - if !config.team_asks.contains_key(&self.text) { - bail!( - "unrecognized team ask {:?}, team asks must be one of the following:\n{}", - self.text, - config - .team_asks - .iter() - .map(|(ask, TeamAskDetails { about, .. })| { - format!("* {ask:?}, meaning team should {about}") - }) - .collect::>() - .join("\n"), + if !config.team_asks.contains_key(&*self.text) { + return Err( + Error::new_str(self.text.as_ref().map(|_| "unrecognized team ask")).wrap_str( + Spanned::here(format!( + "team asks must be one of the following:\n{}", + config + .team_asks + .iter() + .map(|(ask, TeamAskDetails { about, .. })| { + format!("* {ask:?}, meaning team should {about}") + }) + .collect::>() + .join("\n") + )), + ), ); } asks.push(TeamAsk { link_path: link_path.clone(), - ask_description: self.text.clone(), + ask_description: self.text.content.clone(), goal_titles: goal_titles.clone(), teams, owners: goal_owners.to_string(), @@ -674,14 +676,17 @@ impl PlanItem { } } -fn expect_headers(table: &Table, expected: &[&str]) -> anyhow::Result<()> { +fn expect_headers(table: &Table, expected: &[&str]) -> Result<()> { if table.header != expected { // FIXME: do a diff so we see which headers are missing or extraneous - anyhow::bail!( - "{:?}: unexpected table header, expected `{:?}`, found `{:?}`", - table.header[0], - expected, - table.header.iter().map(|h| &h.content), + + return Err( + Error::new_str(table.header[0].as_ref().map(|_| "unexpected table header")).wrap_str( + Spanned::here(format!( + "expected `{expected:?}`, found `{:?}`", + table.header.iter().map(|h| &h.content), + )), + ), ); } diff --git a/crates/rust-project-goals/src/markwaydown.rs b/crates/rust-project-goals/src/markwaydown.rs index c2782711..e866a187 100644 --- a/crates/rust-project-goals/src/markwaydown.rs +++ b/crates/rust-project-goals/src/markwaydown.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, path::Path}; -use spanned::Spanned; +use spanned::{Error, Result, Spanned}; use crate::util; @@ -29,13 +29,13 @@ pub struct Table { pub rows: Vec>>, } -pub fn parse(path: impl AsRef) -> anyhow::Result> { +pub fn parse(path: impl AsRef) -> Result> { let path = path.as_ref(); let text = Spanned::read_str_from_file(path).transpose()?; parse_text(text.as_ref().map(|s| s.as_ref())) } -pub fn parse_text(text: Spanned<&str>) -> anyhow::Result> { +pub fn parse_text(text: Spanned<&str>) -> Result> { let mut result = vec![]; let mut open_section = None; let mut open_table = None; @@ -68,11 +68,11 @@ pub fn parse_text(text: Spanned<&str>) -> anyhow::Result> { if let Some(table) = &mut open_table { if row.len() > table.header.len() { - return Err(anyhow::anyhow!( - "{}: too many columns in table, expected no more than {}", - row[table.header.len()].span, + spanned::bail!( + row[table.header.len()], + "too many columns in table, expected no more than {}", table.header.len() - )); + ); } while row.len() < table.header.len() { @@ -93,25 +93,23 @@ pub fn parse_text(text: Spanned<&str>) -> anyhow::Result> { CategorizeLine::TableDashRow(dashes) => { if let Some(table) = &open_table { if table.header.len() != dashes.len() { - return Err(anyhow::anyhow!( - "{:?}: invalid number of columns in table, expected {}", + spanned::bail!( dashes.last().unwrap(), + "invalid number of columns in table, expected {}", table.header.len() - )); + ); } if let Some(first) = table.rows.first() { - return Err(anyhow::anyhow!( - "{:?}: did not expect table header here, already saw table rows: {:?}", - dashes[0].span, - first[0].span, - )); + return Err(Error::new_str( + dashes[0] + .as_ref() + .map(|_| "did not expect table header here"), + ) + .wrap_str(first[0].as_ref().map(|_| "already saw table row here"))); } } else { - return Err(anyhow::anyhow!( - "{:?}: did not expect table header here", - dashes[0] - )); + spanned::bail!(dashes[0], "did not expect table header here",); } } CategorizeLine::Other => { @@ -197,7 +195,7 @@ impl Table { } /// Modify `path` to replace the lines containing this table with `new_table`. - pub fn overwrite_in_path(&self, path: &Path, new_table: &Table) -> anyhow::Result<()> { + pub fn overwrite_in_path(&self, path: &Path, new_table: &Table) -> Result<()> { let full_text = std::fs::read_to_string(path)?; let mut new_text = full_text[..self.header[0].span.bytes.start].to_string(); diff --git a/crates/rust-project-goals/src/team.rs b/crates/rust-project-goals/src/team.rs index 5a06f6c7..97c4094a 100644 --- a/crates/rust-project-goals/src/team.rs +++ b/crates/rust-project-goals/src/team.rs @@ -4,16 +4,17 @@ use rust_team_data::v1; use serde::de::DeserializeOwned; use crate::util::in_thread; +use spanned::{Error, Result}; trait Load { - fn load(&self, op: impl FnOnce() -> anyhow::Result) -> anyhow::Result<&T>; + fn load(&self, op: impl FnOnce() -> Result) -> Result<&T>; } -impl Load for OnceLock> { - fn load(&self, op: impl FnOnce() -> anyhow::Result) -> anyhow::Result<&T> { +impl Load for OnceLock> { + fn load(&self, op: impl FnOnce() -> Result) -> Result<&T> { match self.get_or_init(op) { Ok(data) => Ok(data), - Err(e) => Err(anyhow::anyhow!("failed to fetch: {e:?}")), + Err(e) => Err(Error::str(format!("failed to fetch: {e:?}"))), } } } @@ -27,8 +28,8 @@ pub struct PersonData { } /// Given a username like `@foo` finds the corresponding person data (if any). -pub fn get_person_data(username: &str) -> anyhow::Result> { - static DATA: OnceLock>> = OnceLock::new(); +pub fn get_person_data(username: &str) -> Result> { + static DATA: OnceLock>> = OnceLock::new(); let people = DATA.load(|| { let data: v1::People = fetch("people.json")?; Ok(data @@ -58,12 +59,12 @@ impl std::fmt::Display for TeamName { } } -pub fn get_team_names() -> anyhow::Result> { +pub fn get_team_names() -> Result> { Ok(get_teams()?.keys()) } -fn get_teams() -> anyhow::Result<&'static BTreeMap> { - static DATA: OnceLock>> = OnceLock::new(); +fn get_teams() -> Result<&'static BTreeMap> { + static DATA: OnceLock>> = OnceLock::new(); DATA.load(|| { let teams: v1::Teams = fetch("teams.json")?; Ok(teams @@ -74,7 +75,7 @@ fn get_teams() -> anyhow::Result<&'static BTreeMap> { }) } -pub fn get_team_name(team_name: &str) -> anyhow::Result> { +pub fn get_team_name(team_name: &str) -> Result> { let team_name = TeamName(team_name.to_string()); Ok(get_teams()?.get_key_value(&team_name).map(|(key, _)| key)) } @@ -112,7 +113,7 @@ impl TeamName { } } -fn fetch(path: &str) -> anyhow::Result +fn fetch(path: &str) -> Result where T: DeserializeOwned + Send, { diff --git a/crates/rust-project-goals/src/util.rs b/crates/rust-project-goals/src/util.rs index 04481009..d8bff269 100644 --- a/crates/rust-project-goals/src/util.rs +++ b/crates/rust-project-goals/src/util.rs @@ -4,7 +4,7 @@ use std::{ path::{Path, PathBuf}, }; -use spanned::Spanned; +use spanned::{Result, Spanned}; use walkdir::WalkDir; pub const ARROW: &str = "↳"; @@ -64,12 +64,12 @@ pub struct GithubUserInfo { } impl GithubUserInfo { - pub fn load(login: &str) -> anyhow::Result { + pub fn load(login: &str) -> Result { Self::github_request(login) } - fn github_request(login: &str) -> anyhow::Result { - in_thread(|| { + fn github_request(login: &str) -> Result { + in_thread(|| -> Result<_> { // FIXME: cache this in the target directory or something use reqwest::header::USER_AGENT; let url = format!("https://api.github.com/users/{}", &login[1..]); @@ -96,9 +96,9 @@ pub fn commas(iter: impl IntoIterator) -> String { /// Returns all markdown files in `directory_path` as `(absolute, relative)` pairs, /// where `relative` is relative to `directory_path`. -pub fn markdown_files(directory_path: &Path) -> anyhow::Result> { +pub fn markdown_files(directory_path: &Path) -> Result> { if !directory_path.is_dir() { - anyhow::bail!("`{}` is not a directory", directory_path.display()); + spanned::bail_here!("`{}` is not a directory", directory_path.display()); } let mut files = vec![];