Skip to main content

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}