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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy