From a1220c63e3a88912fafe685f57532ec57c393e99 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 12 Jun 2023 12:53:55 -0400 Subject: [PATCH] Cleanup (#1566) --- src/analyzer.rs | 2 +- src/compile_error.rs | 289 ++++++++++++++++--------------------------- src/config.rs | 1 + src/enclosure.rs | 3 - src/error.rs | 73 ++++------- src/function.rs | 7 +- src/lexer.rs | 4 +- src/lib.rs | 8 +- src/list.rs | 3 - src/parser.rs | 13 +- src/run.rs | 1 + src/settings.rs | 1 + tests/edit.rs | 7 +- tests/test.rs | 6 +- 14 files changed, 153 insertions(+), 265 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index 6899796..8db272e 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -43,7 +43,7 @@ impl<'src> Analyzer<'src> { let settings = Settings::from_setting_iter(self.sets.into_iter().map(|(_, set)| set.value)); - let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Default::default(); + let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Table::default(); AssignmentResolver::resolve_assignments(&self.assignments)?; diff --git a/src/compile_error.rs b/src/compile_error.rs index cc81d7d..7a0d6f9 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -24,37 +24,27 @@ impl Display for CompileError<'_> { use CompileErrorKind::*; match &*self.kind { - AliasInvalidAttribute { alias, attr } => { - write!( - f, - "Alias {} has an invalid attribute `{}`", - alias, - attr.to_str() - )?; - } - AliasShadowsRecipe { alias, recipe_line } => { - write!( - f, - "Alias `{}` defined on line {} shadows recipe `{}` defined on line {}", - alias, - self.token.line.ordinal(), - alias, - recipe_line.ordinal(), - )?; - } - BacktickShebang => { - write!(f, "Backticks may not start with `#!`")?; - } + AliasInvalidAttribute { alias, attr } => write!( + f, + "Alias {alias} has an invalid attribute `{}`", + attr.to_str(), + ), + AliasShadowsRecipe { alias, recipe_line } => write!( + f, + "Alias `{alias}` defined on line {} shadows recipe `{alias}` defined on line {}", + self.token.line.ordinal(), + recipe_line.ordinal(), + ), + BacktickShebang => write!(f, "Backticks may not start with `#!`"), CircularRecipeDependency { recipe, ref circle } => { if circle.len() == 2 { - write!(f, "Recipe `{recipe}` depends on itself")?; + write!(f, "Recipe `{recipe}` depends on itself") } else { write!( f, - "Recipe `{}` has circular dependency `{}`", - recipe, + "Recipe `{recipe}` has circular dependency `{}`", circle.join(" -> ") - )?; + ) } } CircularVariableDependency { @@ -62,14 +52,13 @@ impl Display for CompileError<'_> { ref circle, } => { if circle.len() == 2 { - write!(f, "Variable `{variable}` is defined in terms of itself")?; + write!(f, "Variable `{variable}` is defined in terms of itself") } else { write!( f, - "Variable `{}` depends on its own value: `{}`", - variable, - circle.join(" -> ") - )?; + "Variable `{variable}` depends on its own value: `{}`", + circle.join(" -> "), + ) } } DependencyArgumentCountMismatch { @@ -80,206 +69,146 @@ impl Display for CompileError<'_> { } => { write!( f, - "Dependency `{}` got {} {} but takes ", - dependency, - found, + "Dependency `{dependency}` got {found} {} but takes ", Count("argument", *found), )?; if min == max { let expected = min; - write!(f, "{expected} {}", Count("argument", *expected))?; + write!(f, "{expected} {}", Count("argument", *expected)) } else if found < min { - write!(f, "at least {min} {}", Count("argument", *min))?; + write!(f, "at least {min} {}", Count("argument", *min)) } else { - write!(f, "at most {max} {}", Count("argument", *max))?; + write!(f, "at most {max} {}", Count("argument", *max)) } } - DuplicateAlias { alias, first } => { - write!( - f, - "Alias `{}` first defined on line {} is redefined on line {}", - alias, - first.ordinal(), - self.token.line.ordinal(), - )?; - } - DuplicateAttribute { attribute, first } => { - write!( - f, - "Recipe attribute `{}` first used on line {} is duplicated on line {}", - attribute, - first.ordinal(), - self.token.line.ordinal(), - )?; - } + DuplicateAlias { alias, first } => write!( + f, + "Alias `{alias}` first defined on line {} is redefined on line {}", + first.ordinal(), + self.token.line.ordinal(), + ), + DuplicateAttribute { attribute, first } => write!( + f, + "Recipe attribute `{attribute}` first used on line {} is duplicated on line {}", + first.ordinal(), + self.token.line.ordinal(), + ), DuplicateParameter { recipe, parameter } => { - write!(f, "Recipe `{recipe}` has duplicate parameter `{parameter}`")?; - } - DuplicateRecipe { recipe, first } => { - write!( - f, - "Recipe `{}` first defined on line {} is redefined on line {}", - recipe, - first.ordinal(), - self.token.line.ordinal() - )?; - } - DuplicateSet { setting, first } => { - write!( - f, - "Setting `{}` first set on line {} is redefined on line {}", - setting, - first.ordinal(), - self.token.line.ordinal(), - )?; + write!(f, "Recipe `{recipe}` has duplicate parameter `{parameter}`") } + DuplicateRecipe { recipe, first } => write!( + f, + "Recipe `{recipe}` first defined on line {} is redefined on line {}", + first.ordinal(), + self.token.line.ordinal(), + ), + DuplicateSet { setting, first } => write!( + f, + "Setting `{setting}` first set on line {} is redefined on line {}", + first.ordinal(), + self.token.line.ordinal(), + ), DuplicateVariable { variable } => { - write!(f, "Variable `{variable}` has multiple definitions")?; + write!(f, "Variable `{variable}` has multiple definitions") } ExpectedKeyword { expected, found } => { + let expected = List::or_ticked(expected); if found.kind == TokenKind::Identifier { write!( f, - "Expected keyword {} but found identifier `{}`", - List::or_ticked(expected), + "Expected keyword {expected} but found identifier `{}`", found.lexeme() - )?; + ) } else { - write!( - f, - "Expected keyword {} but found `{}`", - List::or_ticked(expected), - found.kind - )?; + write!(f, "Expected keyword {expected} but found `{}`", found.kind) } } - ExtraLeadingWhitespace => { - write!(f, "Recipe line has extra leading whitespace")?; - } + ExtraLeadingWhitespace => write!(f, "Recipe line has extra leading whitespace"), FunctionArgumentCountMismatch { function, found, expected, - } => { - write!( - f, - "Function `{}` called with {} {} but takes {}", - function, - found, - Count("argument", *found), - expected.display(), - )?; - } - InconsistentLeadingWhitespace { expected, found } => { - write!( - f, - "Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \ + } => write!( + f, + "Function `{function}` called with {found} {} but takes {}", + Count("argument", *found), + expected.display(), + ), + InconsistentLeadingWhitespace { expected, found } => write!( + f, + "Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \ line with `{}`", - ShowWhitespace(expected), - ShowWhitespace(found) - )?; - } - Internal { ref message } => { - write!( - f, - "Internal error, this may indicate a bug in just: {message}\n\ + ShowWhitespace(expected), + ShowWhitespace(found) + ), + Internal { ref message } => write!( + f, + "Internal error, this may indicate a bug in just: {message}\n\ consider filing an issue: https://github.com/casey/just/issues/new" - )?; - } - InvalidEscapeSequence { character } => { - let representation = match character { + ), + InvalidEscapeSequence { character } => write!( + f, + "`\\{}` is not a valid escape sequence", + match character { '`' => r"\`".to_owned(), '\\' => r"\".to_owned(), '\'' => r"'".to_owned(), '"' => r#"""#.to_owned(), _ => character.escape_default().collect(), - }; - write!(f, "`\\{representation}` is not a valid escape sequence")?; - } + } + ), MismatchedClosingDelimiter { open, open_line, close, - } => { - write!( - f, - "Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)", - close.close(), - open.open(), - open_line.ordinal(), - )?; - } - MixedLeadingWhitespace { whitespace } => { - write!( - f, - "Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \ + } => write!( + f, + "Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)", + close.close(), + open.open(), + open_line.ordinal(), + ), + MixedLeadingWhitespace { whitespace } => write!( + f, + "Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \ consist of tabs or spaces, but not both", - ShowWhitespace(whitespace) - )?; - } + ShowWhitespace(whitespace) + ), ParameterFollowsVariadicParameter { parameter } => { - write!(f, "Parameter `{parameter}` follows variadic parameter")?; - } - ParsingRecursionDepthExceeded => { - write!(f, "Parsing recursion depth exceeded")?; - } - RequiredParameterFollowsDefaultParameter { parameter } => { - write!( - f, - "Non-default parameter `{parameter}` follows default parameter" - )?; - } - UndefinedVariable { variable } => { - write!(f, "Variable `{variable}` not defined")?; - } - UnexpectedCharacter { expected } => { - write!(f, "Expected character `{expected}`")?; + write!(f, "Parameter `{parameter}` follows variadic parameter") } + ParsingRecursionDepthExceeded => write!(f, "Parsing recursion depth exceeded"), + RequiredParameterFollowsDefaultParameter { parameter } => write!( + f, + "Non-default parameter `{parameter}` follows default parameter" + ), + UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), + UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"), UnexpectedClosingDelimiter { close } => { - write!(f, "Unexpected closing delimiter `{}`", close.close())?; + write!(f, "Unexpected closing delimiter `{}`", close.close()) } UnexpectedEndOfToken { expected } => { - write!(f, "Expected character `{expected}` but found end-of-file")?; + write!(f, "Expected character `{expected}` but found end-of-file") } UnexpectedToken { ref expected, found, - } => { - write!(f, "Expected {}, but found {found}", List::or(expected))?; - } + } => write!(f, "Expected {}, but found {found}", List::or(expected)), UnknownAliasTarget { alias, target } => { - write!(f, "Alias `{alias}` has an unknown target `{target}`")?; - } - UnknownAttribute { attribute } => { - write!(f, "Unknown attribute `{attribute}`")?; + write!(f, "Alias `{alias}` has an unknown target `{target}`") } + UnknownAttribute { attribute } => write!(f, "Unknown attribute `{attribute}`"), UnknownDependency { recipe, unknown } => { - write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`",)?; - } - UnknownFunction { function } => { - write!(f, "Call to unknown function `{function}`")?; - } - UnknownSetting { setting } => { - write!(f, "Unknown setting `{setting}`")?; - } - UnknownStartOfToken => { - write!(f, "Unknown start of token:")?; - } - UnpairedCarriageReturn => { - write!(f, "Unpaired carriage return")?; - } - UnterminatedBacktick => { - write!(f, "Unterminated backtick")?; - } - UnterminatedInterpolation => { - write!(f, "Unterminated interpolation")?; - } - UnterminatedString => { - write!(f, "Unterminated string")?; + write!(f, "Recipe `{recipe}` has unknown dependency `{unknown}`") } + UnknownFunction { function } => write!(f, "Call to unknown function `{function}`"), + UnknownSetting { setting } => write!(f, "Unknown setting `{setting}`"), + UnknownStartOfToken => write!(f, "Unknown start of token:"), + UnpairedCarriageReturn => write!(f, "Unpaired carriage return"), + UnterminatedBacktick => write!(f, "Unterminated backtick"), + UnterminatedInterpolation => write!(f, "Unterminated interpolation"), + UnterminatedString => write!(f, "Unterminated string"), } - - Ok(()) } } diff --git a/src/config.rs b/src/config.rs index 069a9a8..abd24cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,7 @@ pub(crate) const CHOOSE_HELP: &str = "Select one or more recipes to run using a of $JUST_CHOOSER, falling back to `fzf`"; #[derive(Debug, PartialEq)] +#[allow(clippy::struct_excessive_bools)] pub(crate) struct Config { pub(crate) check: bool, pub(crate) color: Color, diff --git a/src/enclosure.rs b/src/enclosure.rs index 12c715b..d1cf3ea 100644 --- a/src/enclosure.rs +++ b/src/enclosure.rs @@ -1,6 +1,3 @@ -// `Self` cannot be used where type takes generic arguments -#![allow(clippy::use_self)] - use super::*; pub struct Enclosure { diff --git a/src/error.rs b/src/error.rs index 413076d..b5ff0f1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -238,30 +238,21 @@ impl<'src> ColorDisplay for Error<'src> { let expected = min; write!( f, - "Recipe `{}` got {} {} but {}takes {}", - recipe, - found, + "Recipe `{recipe}` got {found} {} but {}takes {expected}", Count("argument", *found), - if expected < found { "only " } else { "" }, - expected + if expected < found { "only " } else { "" } )?; } else if found < min { write!( f, - "Recipe `{}` got {} {} but takes at least {}", - recipe, - found, - Count("argument", *found), - min + "Recipe `{recipe}` got {found} {} but takes at least {min}", + Count("argument", *found) )?; } else if found > max { write!( f, - "Recipe `{}` got {} {} but takes at most {}", - recipe, - found, - Count("argument", *found), - max + "Recipe `{recipe}` got {found} {} but takes at most {max}", + Count("argument", *found) )?; } } @@ -306,35 +297,29 @@ impl<'src> ColorDisplay for Error<'src> { } => { write!( f, - "Chooser `{} {} {}` invocation failed: {}", - shell_binary, - shell_arguments, + "Chooser `{shell_binary} {shell_arguments} {}` invocation failed: {io_error}", chooser.to_string_lossy(), - io_error, )?; } ChooserRead { chooser, io_error } => { write!( f, - "Failed to read output from chooser `{}`: {}", - chooser.to_string_lossy(), - io_error + "Failed to read output from chooser `{}`: {io_error}", + chooser.to_string_lossy() )?; } ChooserStatus { chooser, status } => { write!( f, - "Chooser `{}` failed: {}", - chooser.to_string_lossy(), - status + "Chooser `{}` failed: {status}", + chooser.to_string_lossy() )?; } ChooserWrite { chooser, io_error } => { write!( f, - "Failed to write to chooser `{}`: {}", - chooser.to_string_lossy(), - io_error + "Failed to write to chooser `{}`: {io_error}", + chooser.to_string_lossy() )?; } CircularInclude { current, include } => { @@ -365,13 +350,12 @@ impl<'src> ColorDisplay for Error<'src> { } => { write!( f, - "Failed to invoke {}: {}", + "Failed to invoke {}: {io_error}", iter::once(binary) .chain(arguments) .map(|value| Enclosure::tick(value.to_string_lossy()).to_string()) .collect::>() .join(" "), - io_error, )?; } CommandStatus { @@ -381,13 +365,12 @@ impl<'src> ColorDisplay for Error<'src> { } => { write!( f, - "Command {} failed: {}", + "Command {} failed: {status}", iter::once(binary) .chain(arguments) .map(|value| Enclosure::tick(value.to_string_lossy()).to_string()) .collect::>() .join(" "), - status, )?; } Compile { compile_error } => Display::fmt(compile_error, f)?, @@ -446,9 +429,7 @@ impl<'src> ColorDisplay for Error<'src> { } => { write!( f, - "Recipe `{}` cannot be used as default recipe since it requires at least {} {}.", - recipe, - min_arguments, + "Recipe `{recipe}` cannot be used as default recipe since it requires at least {min_arguments} {}.", Count("argument", *min_arguments), )?; } @@ -483,9 +464,8 @@ impl<'src> ColorDisplay for Error<'src> { FunctionCall { function, message } => { write!( f, - "Call to function `{}` failed: {}", - function.lexeme(), - message + "Call to function `{}` failed: {message}", + function.lexeme() )?; } IncludeMissingPath { @@ -532,9 +512,8 @@ impl<'src> ColorDisplay for Error<'src> { Load { io_error, path } => { write!( f, - "Failed to read justfile at `{}`: {}", - path.display(), - io_error + "Failed to read justfile at `{}`: {io_error}", + path.display() )?; } NoChoosableRecipes => { @@ -628,9 +607,8 @@ impl<'src> ColorDisplay for Error<'src> { WriteJustfile { justfile, io_error } => { write!( f, - "Failed to write justfile to `{}`: {}", - justfile.display(), - io_error + "Failed to write justfile to `{}`: {io_error}", + justfile.display() )?; } } @@ -642,12 +620,7 @@ impl<'src> ColorDisplay for Error<'src> { } = self { writeln!(f)?; - write!( - f, - "{}:\n just {}", - color.message().paint("usage"), - recipe - )?; + write!(f, "{}:\n just {recipe}", color.message().paint("usage"))?; for param in parameters { write!(f, " {}", param.color_display(color))?; } diff --git a/src/function.rs b/src/function.rs index e646efb..7138a47 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,6 +1,3 @@ -#![allow(unknown_lints)] -#![allow(clippy::unnecessary_wraps)] - use { super::*, heck::{ @@ -208,7 +205,7 @@ fn join( fn just_executable(_context: &FunctionContext) -> Result { let exe_path = - std::env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?; + env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?; exe_path.to_str().map(str::to_owned).ok_or_else(|| { format!( @@ -323,7 +320,7 @@ fn sha256_file(context: &FunctionContext, path: &str) -> Result use sha2::{Digest, Sha256}; let justpath = context.search.working_directory.join(path); let mut hasher = Sha256::new(); - let mut file = std::fs::File::open(&justpath) + let mut file = fs::File::open(&justpath) .map_err(|err| format!("Failed to open file at `{:?}`: {err}", &justpath.to_str()))?; std::io::copy(&mut file, &mut hasher) .map_err(|err| format!("Failed to read file at `{:?}`: {err}", &justpath.to_str()))?; diff --git a/src/lexer.rs b/src/lexer.rs index ebc04e5..55de456 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -208,7 +208,7 @@ impl<'src> Lexer<'src> { }; CompileError::new( token, - CompileErrorKind::Internal { + Internal { message: message.into(), }, ) @@ -773,7 +773,7 @@ impl<'src> Lexer<'src> { /// Lex a backtick, cooked string, or raw string. /// - /// Backtick: `[^`]*` + /// Backtick: ``[^`]*`` /// Cooked string: "[^"]*" # also processes escape sequences /// Raw string: '[^']*' fn lex_string(&mut self) -> CompileResult<'src, ()> { diff --git a/src/lib.rs b/src/lib.rs index 2a05d48..db8f554 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,10 @@ #![deny(clippy::all, clippy::pedantic)] #![allow( - clippy::default_trait_access, - clippy::doc_markdown, clippy::enum_glob_use, clippy::let_underscore_untyped, - clippy::missing_errors_doc, clippy::needless_pass_by_value, - clippy::non_ascii_literal, - clippy::shadow_unrelated, - clippy::struct_excessive_bools, clippy::too_many_lines, - clippy::type_repetition_in_bounds, + clippy::unnecessary_wraps, clippy::wildcard_imports )] diff --git a/src/list.rs b/src/list.rs index 2974b00..38f2a0c 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,6 +1,3 @@ -// `Self` cannot be used where type takes generic arguments -#![allow(clippy::use_self)] - use super::*; pub struct List + Clone> { diff --git a/src/parser.rs b/src/parser.rs index 0ac7d36..066bdca 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -174,15 +174,14 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { if next.kind != Identifier { Err(self.internal_error(format!( - "Presumed next token would have kind {}, but found {}", - Identifier, next.kind + "Presumed next token would have kind {Identifier}, but found {}", + next.kind ))?) } else if keyword == next.lexeme() { Ok(()) } else { Err(self.internal_error(format!( - "Presumed next token would have lexeme \"{}\", but found \"{}\"", - keyword, + "Presumed next token would have lexeme \"{keyword}\", but found \"{}\"", next.lexeme(), ))?) } @@ -196,8 +195,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { Ok(next) } else { Err(self.internal_error(format!( - "Presumed next token would have kind {:?}, but found {:?}", - kind, next.kind + "Presumed next token would have kind {kind:?}, but found {:?}", + next.kind ))?) } } @@ -869,7 +868,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } attributes.insert(attribute, name.line); - if !self.accepted(TokenKind::Comma)? { + if !self.accepted(Comma)? { break; } } diff --git a/src/run.rs b/src/run.rs index a1a18c2..fd8176c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,6 +1,7 @@ use super::*; /// Main entry point into just binary. +#[allow(clippy::missing_errors_doc)] pub fn run() -> Result<(), i32> { #[cfg(windows)] ansi_term::enable_ansi_support().ok(); diff --git a/src/settings.rs b/src/settings.rs index 8a7d919..508cfb0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -6,6 +6,7 @@ pub(crate) const WINDOWS_POWERSHELL_SHELL: &str = "powershell.exe"; pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"]; #[derive(Debug, PartialEq, Serialize, Default)] +#[allow(clippy::struct_excessive_bools)] pub(crate) struct Settings<'src> { pub(crate) allow_duplicate_recipes: bool, pub(crate) dotenv_load: Option, diff --git a/tests/edit.rs b/tests/edit.rs index aa26d2d..75010d3 100644 --- a/tests/edit.rs +++ b/tests/edit.rs @@ -116,7 +116,7 @@ fn editor_precedence() { assert_stdout(&output, JUSTFILE); let cat = which("cat").unwrap(); - let vim = tmp.path().join(format!("vim{}", env::consts::EXE_SUFFIX)); + let vim = tmp.path().join(format!("vim{}", EXE_SUFFIX)); #[cfg(unix)] std::os::unix::fs::symlink(cat, vim).unwrap(); @@ -154,7 +154,7 @@ fn editor_working_directory() { let editor = tmp.path().join("editor"); let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700); - std::fs::set_permissions(&editor, permissions).unwrap(); + fs::set_permissions(&editor, permissions).unwrap(); let output = Command::new(executable_path("just")) .current_dir(tmp.path().join("child")) @@ -164,8 +164,7 @@ fn editor_working_directory() { .unwrap(); let want = format!( - "{}{}\n", - JUSTFILE, + "{JUSTFILE}{}\n", tmp.path().canonicalize().unwrap().display() ); diff --git a/tests/test.rs b/tests/test.rs index dabb520..6c86f0a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -169,8 +169,8 @@ impl Test { pub(crate) fn write(self, path: impl AsRef, content: impl AsRef<[u8]>) -> Self { let path = self.tempdir.path().join(path); - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - std::fs::write(path, content).unwrap(); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, content).unwrap(); self } } @@ -224,7 +224,7 @@ impl Test { fn compare(name: &str, have: T, want: T) -> bool { let equal = have == want; if !equal { - eprintln!("Bad {}: {}", name, Comparison::new(&have, &want)); + eprintln!("Bad {name}: {}", Comparison::new(&have, &want)); } equal }