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