Skip to content

Commit 3cda38d

Browse files
authored
fix: avoid crash when encountering tsconfig circular extends (#570)
When there's a circular `tsconfig` extends, oxc-resolver crashed with `Segmentation fault (core dumped)`. This PR fixes that. In this case, TypeScript outputs errors like: ``` $ pnpm tsc --noEmit -p tsconfig_self_reference.json error TS18000: Circularity detected while resolving configuration: /home/green/workspace/oxc-resolver/tests/tsconfig_self_reference.json -> /home/green/workspace/oxc-resolver/tests/tsconfig_self_reference.json $ pnpm tsc --noEmit -p tsconfig_circular_reference_a.json error TS18000: Circularity detected while resolving configuration: /home/green/workspace/oxc-resolver/tests/tsconfig_circular_reference_a.json -> /home/green/workspace/oxc-resolver/tests/tsconfig_circular_reference_b.json -> /home/green/workspace/oxc-resolver/tests/tsconfig_circular_reference_a.json ```
1 parent 97a0723 commit 3cda38d

File tree

7 files changed

+132
-11
lines changed

7 files changed

+132
-11
lines changed

src/error.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
use std::{io, path::PathBuf, sync::Arc};
1+
use std::{
2+
fmt::{self, Debug, Display},
3+
io,
4+
path::PathBuf,
5+
sync::Arc,
6+
};
27

38
use thiserror::Error;
49

@@ -38,6 +43,10 @@ pub enum ResolveError {
3843
#[error("Tsconfig's project reference path points to this tsconfig {0}")]
3944
TsconfigSelfReference(PathBuf),
4045

46+
/// Occurs when tsconfig extends configs circularly
47+
#[error("Tsconfig extends configs circularly: {0}")]
48+
TsconfigCircularExtend(CircularPathBufs),
49+
4150
#[error("{0}")]
4251
IOError(IOError),
4352

@@ -162,6 +171,27 @@ impl From<io::Error> for ResolveError {
162171
}
163172
}
164173

174+
#[derive(Debug, Clone, PartialEq, Eq)]
175+
pub struct CircularPathBufs(Vec<PathBuf>);
176+
177+
impl Display for CircularPathBufs {
178+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179+
for (i, path) in self.0.iter().enumerate() {
180+
if i != 0 {
181+
write!(f, " -> ")?;
182+
}
183+
path.fmt(f)?;
184+
}
185+
Ok(())
186+
}
187+
}
188+
189+
impl From<Vec<PathBuf>> for CircularPathBufs {
190+
fn from(value: Vec<PathBuf>) -> Self {
191+
Self(value)
192+
}
193+
}
194+
165195
#[test]
166196
fn test_into_io_error() {
167197
use std::io::{self, ErrorKind};

src/lib.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ mod path;
6363
mod resolution;
6464
mod specifier;
6565
mod tsconfig;
66+
mod tsconfig_context;
6667
#[cfg(feature = "fs_cache")]
6768
mod tsconfig_serde;
6869
#[cfg(target_os = "windows")]
@@ -109,7 +110,10 @@ pub use crate::{
109110
resolution::{ModuleType, Resolution},
110111
tsconfig::{CompilerOptions, CompilerOptionsPathsMap, ProjectReference, TsConfig},
111112
};
112-
use crate::{context::ResolveContext as Ctx, path::SLASH_START, specifier::Specifier};
113+
use crate::{
114+
context::ResolveContext as Ctx, path::SLASH_START, specifier::Specifier,
115+
tsconfig_context::TsconfigResolveContext,
116+
};
113117

114118
type ResolveResult<Cp> = Result<Option<Cp>, ResolveError>;
115119

@@ -207,7 +211,12 @@ impl<C: Cache> ResolverGeneric<C> {
207211
/// * See [ResolveError]
208212
pub fn resolve_tsconfig<P: AsRef<Path>>(&self, path: P) -> Result<Arc<C::Tc>, ResolveError> {
209213
let path = path.as_ref();
210-
self.load_tsconfig(true, path, &TsconfigReferences::Auto)
214+
self.load_tsconfig(
215+
true,
216+
path,
217+
&TsconfigReferences::Auto,
218+
&mut TsconfigResolveContext::default(),
219+
)
211220
}
212221

213222
/// Resolve `specifier` at absolute `path` with [ResolveContext]
@@ -1224,23 +1233,36 @@ impl<C: Cache> ResolverGeneric<C> {
12241233
root: bool,
12251234
path: &Path,
12261235
references: &TsconfigReferences,
1236+
ctx: &mut TsconfigResolveContext,
12271237
) -> Result<Arc<C::Tc>, ResolveError> {
12281238
self.cache.get_tsconfig(root, path, |tsconfig| {
12291239
let directory = self.cache.value(tsconfig.directory());
12301240
tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig");
12311241

1242+
if ctx.is_already_extended(tsconfig.path()) {
1243+
return Err(ResolveError::TsconfigCircularExtend(
1244+
ctx.get_extended_configs_with(tsconfig.path().to_path_buf()).into(),
1245+
));
1246+
}
1247+
12321248
// Extend tsconfig
12331249
let extended_tsconfig_paths = tsconfig
12341250
.extends()
12351251
.map(|specifier| self.get_extended_tsconfig_path(&directory, tsconfig, specifier))
12361252
.collect::<Result<Vec<_>, _>>()?;
1237-
for extended_tsconfig_path in extended_tsconfig_paths {
1238-
let extended_tsconfig = self.load_tsconfig(
1239-
/* root */ false,
1240-
&extended_tsconfig_path,
1241-
&TsconfigReferences::Disabled,
1242-
)?;
1243-
tsconfig.extend_tsconfig(&extended_tsconfig);
1253+
if !extended_tsconfig_paths.is_empty() {
1254+
ctx.with_extended_file(tsconfig.path().to_owned(), |ctx| {
1255+
for extended_tsconfig_path in extended_tsconfig_paths {
1256+
let extended_tsconfig = self.load_tsconfig(
1257+
/* root */ false,
1258+
&extended_tsconfig_path,
1259+
&TsconfigReferences::Disabled,
1260+
ctx,
1261+
)?;
1262+
tsconfig.extend_tsconfig(&extended_tsconfig);
1263+
}
1264+
Result::Ok::<(), ResolveError>(())
1265+
})?;
12441266
}
12451267

12461268
if tsconfig.load_references(references) {
@@ -1280,6 +1302,7 @@ impl<C: Cache> ResolverGeneric<C> {
12801302
/* root */ true,
12811303
&tsconfig_options.config_file,
12821304
&tsconfig_options.references,
1305+
&mut TsconfigResolveContext::default(),
12831306
)?;
12841307
let paths = tsconfig.resolve(cached_path.path(), specifier);
12851308
for path in paths {

src/tsconfig_context.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use std::path::{Path, PathBuf};
2+
3+
#[derive(Default)]
4+
pub struct TsconfigResolveContext {
5+
extended_configs: Vec<PathBuf>,
6+
}
7+
8+
impl TsconfigResolveContext {
9+
pub fn with_extended_file<R, T: FnOnce(&mut Self) -> R>(&mut self, path: PathBuf, cb: T) -> R {
10+
self.extended_configs.push(path);
11+
let result = cb(self);
12+
self.extended_configs.pop();
13+
result
14+
}
15+
16+
pub fn is_already_extended(&self, path: &Path) -> bool {
17+
self.extended_configs.iter().any(|config| config == path)
18+
}
19+
20+
pub fn get_extended_configs_with(&self, path: PathBuf) -> Vec<PathBuf> {
21+
let mut new_vec = Vec::with_capacity(self.extended_configs.len() + 1);
22+
new_vec.extend_from_slice(&self.extended_configs);
23+
new_vec.push(path);
24+
new_vec
25+
}
26+
}

tests/integration_test.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::{env, path::PathBuf};
44

55
use oxc_resolver::{
6-
EnforceExtension, FileSystemOs, FsCache, PackageJson, Resolution, ResolveContext,
6+
EnforceExtension, FileSystemOs, FsCache, PackageJson, Resolution, ResolveContext, ResolveError,
77
ResolveOptions, Resolver,
88
};
99

@@ -52,6 +52,39 @@ fn tsconfig() {
5252
assert_eq!(tsconfig.path, PathBuf::from("./tests/tsconfig.json"));
5353
}
5454

55+
#[test]
56+
fn tsconfig_extends_self_reference() {
57+
let resolver = Resolver::new(ResolveOptions::default());
58+
let err = resolver.resolve_tsconfig("./tests/tsconfig_self_reference.json").unwrap_err();
59+
assert_eq!(
60+
err,
61+
ResolveError::TsconfigCircularExtend(
62+
vec![
63+
"./tests/tsconfig_self_reference.json".into(),
64+
"./tests/tsconfig_self_reference.json".into()
65+
]
66+
.into()
67+
)
68+
);
69+
}
70+
71+
#[test]
72+
fn tsconfig_extends_circular_reference() {
73+
let resolver = Resolver::new(ResolveOptions::default());
74+
let err = resolver.resolve_tsconfig("./tests/tsconfig_circular_reference_a.json").unwrap_err();
75+
assert_eq!(
76+
err,
77+
ResolveError::TsconfigCircularExtend(
78+
vec![
79+
"./tests/tsconfig_circular_reference_a.json".into(),
80+
"./tests/tsconfig_circular_reference_b.json".into(),
81+
"./tests/tsconfig_circular_reference_a.json".into(),
82+
]
83+
.into()
84+
)
85+
);
86+
}
87+
5588
#[cfg(feature = "package_json_raw_json_api")]
5689
#[test]
5790
fn package_json_raw_json_api() {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./tsconfig_circular_reference_b.json"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./tsconfig_circular_reference_a.json"
3+
}

tests/tsconfig_self_reference.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./tsconfig_self_reference.json"
3+
}

0 commit comments

Comments
 (0)