oxc_resolver/
error.rs

1use std::{
2    fmt::{self, Debug, Display},
3    io,
4    path::PathBuf,
5    sync::Arc,
6};
7
8use thiserror::Error;
9
10/// All resolution errors
11///
12/// `thiserror` is used to display meaningful error messages.
13#[derive(Debug, Clone, PartialEq, Error)]
14#[non_exhaustive]
15pub enum ResolveError {
16    /// Ignored path
17    ///
18    /// Derived from ignored path (false value) from browser field in package.json
19    /// ```json
20    /// {
21    ///     "browser": {
22    ///         "./module": false
23    ///     }
24    /// }
25    /// ```
26    /// See <https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module>
27    #[error("Path is ignored {0}")]
28    Ignored(PathBuf),
29
30    /// Module not found
31    #[error("Cannot find module '{0}'")]
32    NotFound(/* specifier */ String),
33
34    /// Matched alias value  not found
35    #[error("Cannot find module '{0}' for matched aliased key '{1}'")]
36    MatchedAliasNotFound(/* specifier */ String, /* alias key */ String),
37
38    /// Tsconfig not found
39    #[error("Tsconfig not found {0}")]
40    TsconfigNotFound(PathBuf),
41
42    /// Tsconfig's project reference path points to it self
43    #[error("Tsconfig's project reference path points to this tsconfig {0}")]
44    TsconfigSelfReference(PathBuf),
45
46    /// Occurs when tsconfig extends configs circularly
47    #[error("Tsconfig extends configs circularly: {0}")]
48    TsconfigCircularExtend(CircularPathBufs),
49
50    #[error("{0}")]
51    IOError(IOError),
52
53    /// Indicates the resulting path won't be able consumable by NodeJS `import` or `require`.
54    /// For example, DOS device path with Volume GUID (`\\?\Volume{...}`) is not supported.
55    #[error("Path {0:?} contains unsupported construct.")]
56    PathNotSupported(PathBuf),
57
58    /// Node.js builtin module when `Options::builtin_modules` is enabled.
59    ///
60    /// `is_runtime_module` can be used to determine whether the request
61    /// was prefixed with `node:` or not.
62    ///
63    /// `resolved` is always prefixed with "node:" in compliance with the ESM specification.
64    #[error("Builtin module {resolved}")]
65    Builtin { resolved: String, is_runtime_module: bool },
66
67    /// All of the aliased extension are not found
68    ///
69    /// Displays `Cannot resolve 'index.mjs' with extension aliases 'index.mts' in ...`
70    #[error("Cannot resolve '{0}' for extension aliases '{1}' in '{2}'")]
71    ExtensionAlias(
72        /* File name */ String,
73        /* Tried file names */ String,
74        /* Path to dir */ PathBuf,
75    ),
76
77    /// The provided path specifier cannot be parsed
78    #[error("{0}")]
79    Specifier(SpecifierError),
80
81    /// JSON parse error
82    #[error("{0:?}")]
83    Json(JSONError),
84
85    #[error(r#"Invalid module "{0}" specifier is not a valid subpath for the "exports" resolution of {1}"#)]
86    InvalidModuleSpecifier(String, PathBuf),
87
88    #[error(r#"Invalid "exports" target "{0}" defined for '{1}' in the package config {2}"#)]
89    InvalidPackageTarget(String, String, PathBuf),
90
91    #[error(r#""{subpath}" is not exported under {conditions} from package {package_path} (see exports field in {package_json_path})"#)]
92    PackagePathNotExported {
93        subpath: String,
94        package_path: PathBuf,
95        package_json_path: PathBuf,
96        conditions: ConditionNames,
97    },
98
99    #[error(r#"Invalid package config "{0}", "exports" cannot contain some keys starting with '.' and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only."#)]
100    InvalidPackageConfig(PathBuf),
101
102    #[error(r#"Default condition should be last one in "{0}""#)]
103    InvalidPackageConfigDefault(PathBuf),
104
105    #[error(r#"Expecting folder to folder mapping. "{0}" should end with "/"#)]
106    InvalidPackageConfigDirectory(PathBuf),
107
108    #[error(r#"Package import specifier "{0}" is not defined in package {1}"#)]
109    PackageImportNotDefined(String, PathBuf),
110
111    #[error("{0} is unimplemented")]
112    Unimplemented(&'static str),
113
114    /// Occurs when alias paths reference each other.
115    #[error("Recursion in resolving")]
116    Recursion,
117
118    #[cfg(feature = "yarn_pnp")]
119    #[error("Failed to find yarn pnp manifest in {0}.")]
120    FailedToFindYarnPnpManifest(PathBuf),
121
122    #[cfg(feature = "yarn_pnp")]
123    #[error("{0}")]
124    YarnPnpError(pnp::Error),
125}
126
127impl ResolveError {
128    #[must_use]
129    pub const fn is_ignore(&self) -> bool {
130        matches!(self, Self::Ignored(_))
131    }
132
133    #[cold]
134    #[must_use]
135    pub fn from_serde_json_error(path: PathBuf, error: &serde_json::Error) -> Self {
136        Self::Json(JSONError {
137            path,
138            message: error.to_string(),
139            line: error.line(),
140            column: error.column(),
141        })
142    }
143}
144
145/// Error for [ResolveError::Specifier]
146#[derive(Debug, Clone, Eq, PartialEq, Error)]
147pub enum SpecifierError {
148    #[error("The specifiers must be a non-empty string. Received \"{0}\"")]
149    Empty(String),
150}
151
152/// JSON error from [serde_json::Error]
153#[derive(Debug, Clone, Eq, PartialEq, Error)]
154#[error("{message}")]
155pub struct JSONError {
156    pub path: PathBuf,
157    pub message: String,
158    pub line: usize,
159    pub column: usize,
160}
161
162#[derive(Debug, Clone, Error)]
163#[error("{0}")]
164pub struct IOError(Arc<io::Error>);
165
166impl PartialEq for IOError {
167    fn eq(&self, other: &Self) -> bool {
168        self.0.kind() == other.0.kind()
169    }
170}
171
172impl From<IOError> for io::Error {
173    #[cold]
174    fn from(error: IOError) -> Self {
175        let io_error = error.0.as_ref();
176        Self::new(io_error.kind(), io_error.to_string())
177    }
178}
179
180impl From<io::Error> for ResolveError {
181    #[cold]
182    fn from(err: io::Error) -> Self {
183        Self::IOError(IOError(Arc::new(err)))
184    }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct CircularPathBufs(Vec<PathBuf>);
189
190impl Display for CircularPathBufs {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        for (i, path) in self.0.iter().enumerate() {
193            if i != 0 {
194                write!(f, " -> ")?;
195            }
196            path.fmt(f)?;
197        }
198        Ok(())
199    }
200}
201
202impl From<Vec<PathBuf>> for CircularPathBufs {
203    #[cold]
204    fn from(value: Vec<PathBuf>) -> Self {
205        Self(value)
206    }
207}
208
209/// Helper type for formatting condition names in error messages
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct ConditionNames(Vec<String>);
212
213impl From<Vec<String>> for ConditionNames {
214    fn from(conditions: Vec<String>) -> Self {
215        Self(conditions)
216    }
217}
218
219impl Display for ConditionNames {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self.0.len() {
222            0 => write!(f, "no conditions"),
223            1 => write!(f, "the condition \"{}\"", self.0[0]),
224            _ => {
225                write!(f, "the conditions ")?;
226                let conditions_str =
227                    self.0.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(", ");
228                write!(f, "[{conditions_str}]")
229            }
230        }
231    }
232}
233
234#[test]
235fn test_into_io_error() {
236    use std::io::{self, ErrorKind};
237    let error_string = "IOError occurred";
238    let string_error = io::Error::new(ErrorKind::Interrupted, error_string.to_string());
239    let string_error2 = io::Error::new(ErrorKind::Interrupted, error_string.to_string());
240    let resolve_io_error: ResolveError = ResolveError::from(string_error2);
241
242    assert_eq!(resolve_io_error, ResolveError::from(string_error));
243    assert_eq!(resolve_io_error.clone(), resolve_io_error);
244    let ResolveError::IOError(io_error) = resolve_io_error else { unreachable!() };
245    assert_eq!(
246        format!("{io_error:?}"),
247        r#"IOError(Custom { kind: Interrupted, error: "IOError occurred" })"#
248    );
249    // fix for https://github.com/web-infra-dev/rspack/issues/4564
250    let std_io_error: io::Error = io_error.into();
251    assert_eq!(std_io_error.kind(), ErrorKind::Interrupted);
252    assert_eq!(std_io_error.to_string(), error_string);
253    assert_eq!(
254        format!("{std_io_error:?}"),
255        r#"Custom { kind: Interrupted, error: "IOError occurred" }"#
256    );
257}
258
259#[test]
260fn test_coverage() {
261    let error = ResolveError::NotFound("x".into());
262    assert_eq!(format!("{error:?}"), r#"NotFound("x")"#);
263    assert_eq!(error.clone(), error);
264
265    let error = ResolveError::Specifier(SpecifierError::Empty("x".into()));
266    assert_eq!(format!("{error:?}"), r#"Specifier(Empty("x"))"#);
267    assert_eq!(error.clone(), error);
268}
269
270#[test]
271fn test_circular_path_bufs_display() {
272    use std::path::PathBuf;
273
274    let paths = vec![
275        PathBuf::from("/foo/tsconfig.json"),
276        PathBuf::from("/bar/tsconfig.json"),
277        PathBuf::from("/baz/tsconfig.json"),
278    ];
279    let circular = CircularPathBufs::from(paths);
280    let display_str = format!("{circular}");
281    assert!(display_str.contains("/foo/tsconfig.json"));
282    assert!(display_str.contains(" -> "));
283    assert!(display_str.contains("/bar/tsconfig.json"));
284    assert!(display_str.contains("/baz/tsconfig.json"));
285}