Skip to main content

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 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 CircularPathBufs {
191    #[must_use]
192    pub fn paths(&self) -> &[PathBuf] {
193        &self.0
194    }
195}
196
197impl Display for CircularPathBufs {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        for (i, path) in self.0.iter().enumerate() {
200            if i != 0 {
201                write!(f, " -> ")?;
202            }
203            path.fmt(f)?;
204        }
205        Ok(())
206    }
207}
208
209impl From<Vec<PathBuf>> for CircularPathBufs {
210    #[cold]
211    fn from(value: Vec<PathBuf>) -> Self {
212        Self(value)
213    }
214}
215
216/// Helper type for formatting condition names in error messages
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct ConditionNames(Vec<String>);
219
220impl ConditionNames {
221    #[must_use]
222    pub fn names(&self) -> &[String] {
223        &self.0
224    }
225}
226
227impl From<Vec<String>> for ConditionNames {
228    fn from(conditions: Vec<String>) -> Self {
229        Self(conditions)
230    }
231}
232
233impl Display for ConditionNames {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        match self.0.len() {
236            0 => write!(f, "no conditions"),
237            1 => write!(f, "the condition \"{}\"", self.0[0]),
238            _ => {
239                write!(f, "the conditions ")?;
240                let conditions_str =
241                    self.0.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(", ");
242                write!(f, "[{conditions_str}]")
243            }
244        }
245    }
246}
247
248#[test]
249fn test_into_io_error() {
250    use std::io::{self, ErrorKind};
251    let error_string = "IOError occurred";
252    let string_error = io::Error::new(ErrorKind::Interrupted, error_string.to_string());
253    let string_error2 = io::Error::new(ErrorKind::Interrupted, error_string.to_string());
254    let resolve_io_error: ResolveError = ResolveError::from(string_error2);
255
256    assert_eq!(resolve_io_error, ResolveError::from(string_error));
257    assert_eq!(resolve_io_error.clone(), resolve_io_error);
258    let ResolveError::IOError(io_error) = resolve_io_error else { unreachable!() };
259    assert_eq!(
260        format!("{io_error:?}"),
261        r#"IOError(Custom { kind: Interrupted, error: "IOError occurred" })"#
262    );
263    // fix for https://github.com/web-infra-dev/rspack/issues/4564
264    let std_io_error: io::Error = io_error.into();
265    assert_eq!(std_io_error.kind(), ErrorKind::Interrupted);
266    assert_eq!(std_io_error.to_string(), error_string);
267    assert_eq!(
268        format!("{std_io_error:?}"),
269        r#"Custom { kind: Interrupted, error: "IOError occurred" }"#
270    );
271}
272
273#[test]
274fn test_coverage() {
275    let error = ResolveError::NotFound("x".into());
276    assert_eq!(format!("{error:?}"), r#"NotFound("x")"#);
277    assert_eq!(error.clone(), error);
278
279    let error = ResolveError::Specifier(SpecifierError::Empty("x".into()));
280    assert_eq!(format!("{error:?}"), r#"Specifier(Empty("x"))"#);
281    assert_eq!(error.clone(), error);
282}
283
284#[test]
285fn test_circular_path_bufs_display() {
286    use std::path::PathBuf;
287
288    let paths = vec![
289        PathBuf::from("/foo/tsconfig.json"),
290        PathBuf::from("/bar/tsconfig.json"),
291        PathBuf::from("/baz/tsconfig.json"),
292    ];
293    let circular = CircularPathBufs::from(paths);
294    let display_str = format!("{circular}");
295    assert!(display_str.contains("/foo/tsconfig.json"));
296    assert!(display_str.contains(" -> "));
297    assert!(display_str.contains("/bar/tsconfig.json"));
298    assert!(display_str.contains("/baz/tsconfig.json"));
299}