Skip to content

Commit 7ffde2c

Browse files
authored
chore: better error reporting (#206)
Signed-off-by: tison <wander4096@gmail.com>
1 parent 9467ed1 commit 7ffde2c

File tree

15 files changed

+285
-168
lines changed

15 files changed

+285
-168
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
test:
4949
strategy:
5050
matrix:
51-
rust-version: ["1.85.0", "stable"]
51+
rust-version: ["1.90.0", "stable"]
5252
name: Build and test
5353
runs-on: ubuntu-24.04
5454
steps:

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44

55
## Unreleased
66

7+
## [6.5.0] 2026-02-09
8+
9+
### Notable changes
10+
11+
* Minimal Supported Rust Version (MSRV) is now 1.90.0.
12+
13+
### Bug fixes
14+
15+
* `hawkeye` CLI now uses hawkeye-fmt of exactly the same version to format headers, instead of using the latest version of `hawkeye-fmt` that may not be compatible with the current version of `hawkeye`.
16+
17+
### Improvements
18+
19+
* Replace `anyhow` with `exn` for more informative error messages.
20+
721
## [6.4.2] 2026-02-07
822

923
## Bug fixes

Cargo.lock

Lines changed: 10 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ members = ["cli", "fmt"]
1717
resolver = "2"
1818

1919
[workspace.package]
20-
version = "6.4.2"
20+
version = "6.5.0"
2121
edition = "2021"
2222
authors = ["tison <wander4096@gmail.com>"]
2323
readme = "README.md"
2424
license = "Apache-2.0"
2525
repository = "https://github.com/korandoru/hawkeye/"
26-
rust-version = "1.85.0"
26+
rust-version = "1.90.0"
2727

2828
[workspace.dependencies]
29-
hawkeye-fmt = { version = "6.4.2", path = "fmt" }
29+
hawkeye-fmt = { version = "=6.5.0", path = "fmt" }
3030

31-
anyhow = { version = "1.0.94" }
3231
build-data = { version = "0.3.0" }
3332
clap = { version = "4.5.23", features = ["derive"] }
3433
const_format = { version = "0.2.34" }
34+
exn = { version = "0.3.0" }
3535
log = { version = "0.4.22", features = ["kv_serde", "serde"] }
3636
shadow-rs = { version = "1.7.0", default-features = false }
3737
toml = { version = "0.9.5" }

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ license.workspace = true
2323
repository.workspace = true
2424

2525
[dependencies]
26-
anyhow = { workspace = true }
2726
clap = { workspace = true }
2827
const_format = { workspace = true }
28+
exn = { workspace = true }
2929
hawkeye-fmt = { workspace = true }
3030
log = { workspace = true }
3131
logforth = { version = "0.29.1", features = ["starter-log"] }

cli/src/subcommand.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use std::path::PathBuf;
1818

1919
use clap::Args;
2020
use clap::Parser;
21+
use exn::Result;
2122
use hawkeye_fmt::document::Document;
23+
use hawkeye_fmt::error::Error;
2224
use hawkeye_fmt::header::matcher::HeaderMatcher;
2325
use hawkeye_fmt::processor::check_license_header;
2426
use hawkeye_fmt::processor::Callback;
@@ -95,11 +97,11 @@ impl Callback for CheckContext {
9597
self.unknown.push(path.display().to_string());
9698
}
9799

98-
fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> anyhow::Result<()> {
100+
fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> Result<(), Error> {
99101
Ok(())
100102
}
101103

102-
fn on_not_matched(&mut self, _: &HeaderMatcher, document: Document) -> anyhow::Result<()> {
104+
fn on_not_matched(&mut self, _: &HeaderMatcher, document: Document) -> Result<(), Error> {
103105
self.missing.push(document.filepath.display().to_string());
104106
Ok(())
105107
}
@@ -149,11 +151,11 @@ impl Callback for FormatContext {
149151
self.unknown.push(path.display().to_string());
150152
}
151153

152-
fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> anyhow::Result<()> {
154+
fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> Result<(), Error> {
153155
Ok(())
154156
}
155157

156-
fn on_not_matched(&mut self, header: &HeaderMatcher, mut doc: Document) -> anyhow::Result<()> {
158+
fn on_not_matched(&mut self, header: &HeaderMatcher, mut doc: Document) -> Result<(), Error> {
157159
if doc.header_detected() {
158160
doc.remove_header();
159161
doc.update_header(header)?;
@@ -221,7 +223,7 @@ struct RemoveContext {
221223
}
222224

223225
impl RemoveContext {
224-
fn remove(&mut self, doc: &mut Document) -> anyhow::Result<()> {
226+
fn remove(&mut self, doc: &mut Document) -> Result<(), Error> {
225227
if !doc.header_detected() {
226228
return Ok(());
227229
}
@@ -244,11 +246,11 @@ impl Callback for RemoveContext {
244246
self.unknown.push(path.display().to_string());
245247
}
246248

247-
fn on_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> anyhow::Result<()> {
249+
fn on_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> Result<(), Error> {
248250
self.remove(&mut doc)
249251
}
250252

251-
fn on_not_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> anyhow::Result<()> {
253+
fn on_not_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> Result<(), Error> {
252254
self.remove(&mut doc)
253255
}
254256
}

fmt/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ license.workspace = true
2323
repository.workspace = true
2424

2525
[dependencies]
26-
anyhow = { workspace = true }
26+
exn = { workspace = true }
2727
gix = { version = "0.78.0", default-features = false, features = [
2828
"blob-diff",
2929
"excludes",

fmt/src/document/factory.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
use std::collections::HashMap;
1616
use std::collections::HashSet;
1717
use std::fs;
18-
use std::io;
1918
use std::path::Path;
2019
use std::path::PathBuf;
2120
use std::time::SystemTime;
2221

23-
use anyhow::Context;
22+
use exn::OptionExt;
23+
use exn::Result;
2424

2525
use crate::config::Mapping;
2626
use crate::document::Attributes;
2727
use crate::document::Document;
28+
use crate::error::Error;
2829
use crate::git::GitFileAttrs;
2930
use crate::header::model::HeaderDef;
3031

@@ -54,7 +55,7 @@ impl DocumentFactory {
5455
}
5556
}
5657

57-
pub fn create_document(&self, filepath: &Path) -> anyhow::Result<Option<Document>> {
58+
pub fn create_document(&self, filepath: &Path) -> Result<Option<Document>, Error> {
5859
let lower_file_name = filepath
5960
.file_name()
6061
.map(|n| n.to_string_lossy().to_lowercase())
@@ -65,11 +66,13 @@ impl DocumentFactory {
6566
.find_map(|m| m.header_type(&lower_file_name))
6667
.unwrap_or_else(|| "unknown".to_string())
6768
.to_lowercase();
68-
let header_def = self
69-
.definitions
70-
.get(&header_type)
71-
.ok_or_else(|| io::Error::other(format!("header type {header_type} not found")))
72-
.with_context(|| format!("cannot create document: {}", filepath.display()))?;
69+
let header_def = self.definitions.get(&header_type).ok_or_raise(|| {
70+
Error::new(format!(
71+
"cannot create document: {}, header type {} not found",
72+
filepath.display(),
73+
header_type
74+
))
75+
})?;
7376

7477
let props = self.properties.clone();
7578

fmt/src/document/mod.rs

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ use std::fs::File;
1919
use std::io::BufRead;
2020
use std::path::PathBuf;
2121

22-
use anyhow::Context;
22+
use exn::ErrorExt;
23+
use exn::Result;
24+
use exn::ResultExt;
2325
use minijinja::context;
2426
use minijinja::Environment;
2527
use serde::Deserialize;
2628
use serde::Serialize;
2729

30+
use crate::error::Error;
2831
use crate::header::matcher::HeaderMatcher;
2932
use crate::header::model::HeaderDef;
3033
use crate::header::parser::parse_header;
@@ -60,7 +63,7 @@ impl Document {
6063
keywords: &[String],
6164
props: HashMap<String, String>,
6265
attrs: Attributes,
63-
) -> anyhow::Result<Option<Self>> {
66+
) -> Result<Option<Self>, Error> {
6467
match FileContent::new(&filepath) {
6568
Ok(content) => Ok(Some(Self {
6669
parser: parse_header(content, &header_def, keywords),
@@ -69,13 +72,15 @@ impl Document {
6972
props,
7073
attrs,
7174
})),
72-
Err(e) => {
73-
if matches!(e.kind(), std::io::ErrorKind::InvalidData) {
75+
Err(err) => {
76+
if matches!(err.kind(), std::io::ErrorKind::InvalidData) {
7477
log::debug!("skip non-textual file: {}", filepath.display());
7578
Ok(None)
7679
} else {
77-
Err(e)
78-
.with_context(|| format!("cannot create document: {}", filepath.display()))
80+
Err(err.raise().raise(Error::new(format!(
81+
"cannot create document: {}",
82+
filepath.display()
83+
))))
7984
}
8085
}
8186
}
@@ -95,7 +100,7 @@ impl Document {
95100
&self,
96101
header: &HeaderMatcher,
97102
strict_check: bool,
98-
) -> anyhow::Result<bool> {
103+
) -> Result<bool, Error> {
99104
if strict_check {
100105
let file_header = {
101106
let mut lines = self.read_file_first_lines(header)?.join("\n");
@@ -115,15 +120,19 @@ impl Document {
115120
}
116121
}
117122

118-
fn read_file_first_lines(&self, header: &HeaderMatcher) -> std::io::Result<Vec<String>> {
119-
let file = File::open(&self.filepath)?;
123+
#[track_caller]
124+
fn read_file_first_lines(&self, header: &HeaderMatcher) -> Result<Vec<String>, Error> {
125+
let make_error = || Error::new("cannot read file first line");
126+
let file = File::open(&self.filepath).or_raise(make_error)?;
120127
std::io::BufReader::new(file)
121128
.lines()
122129
.take(header.header_content_lines_count() + 10)
123130
.collect::<std::io::Result<Vec<_>>>()
131+
.or_raise(make_error)
124132
}
125133

126-
fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> std::io::Result<String> {
134+
#[track_caller]
135+
fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> Result<String, Error> {
127136
let first_lines = self.read_file_first_lines(header)?;
128137
let file_header = first_lines
129138
.join("")
@@ -137,7 +146,7 @@ impl Document {
137146
Ok(file_header)
138147
}
139148

140-
pub fn update_header(&mut self, header: &HeaderMatcher) -> anyhow::Result<()> {
149+
pub fn update_header(&mut self, header: &HeaderMatcher) -> Result<(), Error> {
141150
let header_str = header.build_for_definition(&self.header_def);
142151
let header_str = self.merge_properties(&header_str)?;
143152
let begin_pos = self.parser.begin_pos;
@@ -155,22 +164,24 @@ impl Document {
155164
}
156165
}
157166

158-
pub fn save(&mut self, filepath: Option<&PathBuf>) -> anyhow::Result<()> {
167+
pub fn save(&mut self, filepath: Option<&PathBuf>) -> Result<(), Error> {
159168
let filepath = filepath.unwrap_or(&self.filepath);
160169
fs::write(filepath, self.parser.file_content.content())
161-
.context(format!("cannot save document {}", filepath.display()))
170+
.or_raise(|| Error::new(format!("cannot save document {}", filepath.display())))
162171
}
163172

164-
pub(crate) fn merge_properties(&self, s: &str) -> anyhow::Result<String> {
173+
pub(crate) fn merge_properties(&self, s: &str) -> Result<String, Error> {
165174
let mut env = Environment::new();
166175
env.add_template("template", s)
167-
.context("malformed template")?;
176+
.or_raise(|| Error::new("malformed template"))?;
168177

169178
let tmpl = env.get_template("template").expect("template must exist");
170-
let mut result = tmpl.render(context! {
171-
props => &self.props,
172-
attrs => &self.attrs,
173-
})?;
179+
let mut result = tmpl
180+
.render(context! {
181+
props => &self.props,
182+
attrs => &self.attrs,
183+
})
184+
.or_raise(|| Error::new("cannot render template"))?;
174185
result.push('\n');
175186
Ok(result)
176187
}

fmt/src/error.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2024 tison <wander4096@gmail.com>
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#[derive(Debug)]
16+
pub struct Error {
17+
message: String,
18+
}
19+
20+
impl Error {
21+
pub fn new(message: impl Into<String>) -> Self {
22+
Self {
23+
message: message.into(),
24+
}
25+
}
26+
}
27+
28+
impl std::fmt::Display for Error {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
write!(f, "{}", self.message)
31+
}
32+
}
33+
34+
impl std::error::Error for Error {}

0 commit comments

Comments
 (0)