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