1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Error type of the file-backed [`Ast::from_path`][crate::Ast::from_path].
use crate::MetricsError;
/// Error returned by [`Ast::from_path`][crate::Ast::from_path].
///
/// `from_path` reads, language-detects, and parses a file in one call, so it
/// can fail in more ways than the in-memory [`Ast::parse`][crate::Ast::parse]
/// (which only reports [`MetricsError`]). Unlike [`analyze`][crate::analyze],
/// `from_path` does not silently skip files: every reason it cannot produce a
/// tree surfaces as a distinct variant so the caller — who asked for *this*
/// file's tree — learns why.
///
/// The enum is `#[non_exhaustive]`; match with a trailing `_` arm to stay
/// forward-compatible.
#[non_exhaustive]
#[derive(Debug)]
pub enum FromPathError {
/// The file could not be read (a genuine I/O fault: missing file,
/// permission denied, hardware error). Carries the underlying
/// [`std::io::Error`].
Io(std::io::Error),
/// The path is not valid UTF-8. The path doubles as the resulting
/// [`FuncSpace`][crate::FuncSpace] name (an identifier used as a map key
/// and in JSON output), so a lossy conversion is rejected rather than
/// silently corrupting correlation — mirroring `analyze`'s strict
/// default.
NonUtf8Path,
/// The file is empty, too small, binary, or encoded in an unsupported
/// encoding (UTF-16, invalid UTF-8) — the same files
/// [`analyze`][crate::analyze] skips. `from_path` reuses the library's
/// text reader for byte-exact metric parity with `analyze`, so these
/// inputs cannot yield a tree.
Unreadable,
/// No language is registered for the path (unknown extension and no
/// recognizable shebang / mode line).
UnknownLanguage,
/// The detected language's per-language Cargo feature is not enabled in
/// this build (carries the [`MetricsError::LanguageDisabled`] raised by
/// [`Ast::parse`][crate::Ast::parse]).
Parse(MetricsError),
}
impl std::fmt::Display for FromPathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "could not read file: {e}"),
Self::NonUtf8Path => f.write_str("path is not valid UTF-8"),
Self::Unreadable => {
f.write_str("file is empty, binary, or not valid UTF-8 source text")
}
Self::UnknownLanguage => f.write_str("no language is registered for this path"),
Self::Parse(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for FromPathError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Parse(e) => Some(e),
_ => None,
}
}
}
impl From<MetricsError> for FromPathError {
fn from(e: MetricsError) -> Self {
Self::Parse(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LANG;
use std::error::Error as _;
use std::io::{Error as IoError, ErrorKind};
// `FromPathError`'s `Display` / `source` impls and its `From`
// conversion are part of the stable error surface but are only
// reached on `from_path` failure paths that no other test drives.
// Pin the message shape (substring, not exact wording) and the
// `source` chaining contract, which is what `?`-propagating callers
// and `anyhow`-style reporters rely on.
#[cfg(feature = "rust")]
#[test]
fn from_path_error_display_covers_every_variant() {
let io = FromPathError::Io(IoError::new(ErrorKind::PermissionDenied, "denied"));
assert!(io.to_string().contains("could not read file"));
assert!(io.to_string().contains("denied"));
assert!(
FromPathError::NonUtf8Path
.to_string()
.contains("not valid UTF-8")
);
assert!(
FromPathError::Unreadable
.to_string()
.contains("empty, binary")
);
assert!(
FromPathError::UnknownLanguage
.to_string()
.contains("no language is registered")
);
// `Parse` delegates to the wrapped `MetricsError`'s `Display`.
let parse = FromPathError::Parse(MetricsError::LanguageDisabled(LANG::Rust));
assert_eq!(
parse.to_string(),
MetricsError::LanguageDisabled(LANG::Rust).to_string()
);
}
#[test]
fn from_path_error_source_chains_only_for_wrapping_variants() {
// `Io` and `Parse` wrap an underlying error and must expose it;
// the leaf variants must report no source.
let io = FromPathError::Io(IoError::new(ErrorKind::NotFound, "missing"));
assert!(io.source().is_some(), "Io must chain to the io::Error");
let parse = FromPathError::Parse(MetricsError::EmptyRoot);
assert!(parse.source().is_some(), "Parse must chain to MetricsError");
for leaf in [
FromPathError::NonUtf8Path,
FromPathError::Unreadable,
FromPathError::UnknownLanguage,
] {
assert!(leaf.source().is_none(), "{leaf:?} must report no source");
}
}
#[cfg(feature = "cpp")]
#[test]
fn metrics_error_converts_into_parse_variant() {
let converted: FromPathError = MetricsError::LanguageDisabled(LANG::Cpp).into();
assert!(
matches!(
converted,
FromPathError::Parse(MetricsError::LanguageDisabled(LANG::Cpp))
),
"From<MetricsError> must wrap into Parse"
);
}
}