1use crate::lint::{LintConfig, LintLevel, LintSetting};
2
3use std::{
4 fmt,
5 path::{Component, Path, PathBuf},
6 sync::Arc,
7};
8
9const MAX_JSON_DEPTH: usize = 128;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum PathError {
15 RootIsNotAbsolute { path: PathBuf },
16 PathEscapesRoot { root: PathBuf, path: PathBuf },
17 PathHasNoParent { path: PathBuf },
18}
19
20impl fmt::Display for PathError {
21 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::RootIsNotAbsolute { path } => {
24 write!(
25 formatter,
26 "project root must be absolute: {}",
27 path.display()
28 )
29 }
30 Self::PathEscapesRoot { root, path } => write!(
31 formatter,
32 "path {} escapes project root {}",
33 path.display(),
34 root.display()
35 ),
36 Self::PathHasNoParent { path } => {
37 write!(formatter, "module path has no parent: {}", path.display())
38 }
39 }
40 }
41}
42
43impl std::error::Error for PathError {}
44
45#[derive(Clone, Debug, Eq, Hash, PartialEq)]
50pub struct ProjectRoot {
51 path: PathBuf,
52}
53
54impl ProjectRoot {
55 pub fn new(path: impl AsRef<Path>) -> Result<Self, PathError> {
57 let path = path.as_ref();
58 if !path.is_absolute() {
59 return Err(PathError::RootIsNotAbsolute {
60 path: path.to_path_buf(),
61 });
62 }
63 Ok(Self {
64 path: normalize_absolute(path),
65 })
66 }
67
68 #[must_use]
70 pub fn path(&self) -> &Path {
71 &self.path
72 }
73
74 pub fn resolve(&self, path: impl AsRef<Path>) -> Result<PathBuf, PathError> {
76 self.resolve_from(&self.path, path)
77 }
78
79 pub fn resolve_from(
81 &self,
82 directory: impl AsRef<Path>,
83 path: impl AsRef<Path>,
84 ) -> Result<PathBuf, PathError> {
85 let directory = self.confine(directory)?;
86 let path = path.as_ref();
87 let joined = if path.is_absolute() {
88 path.to_path_buf()
89 } else {
90 directory.join(path)
91 };
92 self.confine(joined)
93 }
94
95 pub fn confine(&self, path: impl AsRef<Path>) -> Result<PathBuf, PathError> {
97 let original = path.as_ref();
98 let absolute = if original.is_absolute() {
99 normalize_absolute(original)
100 } else {
101 normalize_absolute(&self.path.join(original))
102 };
103 if absolute.starts_with(&self.path) {
104 Ok(absolute)
105 } else {
106 Err(PathError::PathEscapesRoot {
107 root: self.path.clone(),
108 path: absolute,
109 })
110 }
111 }
112}
113
114fn normalize_absolute(path: &Path) -> PathBuf {
115 let mut normalized = PathBuf::new();
116 for component in path.components() {
117 match component {
118 Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
119 Component::RootDir => normalized.push(component.as_os_str()),
120 Component::CurDir => {}
121 Component::ParentDir => {
122 normalized.pop();
123 }
124 Component::Normal(part) => normalized.push(part),
125 }
126 }
127 normalized
128}
129
130#[derive(Clone, Debug, Eq, PartialEq)]
132pub enum JsoncErrorKind {
133 UnexpectedEnd,
134 UnexpectedToken,
135 UnterminatedBlockComment,
136 UnterminatedString,
137 InvalidEscape,
138 InvalidUnicodeEscape,
139 LoneSurrogate,
140 UnescapedControlCharacter,
141 InvalidNumber,
142 DuplicateObjectKey { key: Arc<str> },
143 TrailingCharacters,
144 NestingTooDeep,
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct JsoncError {
151 offset: usize,
152 kind: JsoncErrorKind,
153}
154
155impl JsoncError {
156 #[must_use]
157 pub const fn offset(&self) -> usize {
158 self.offset
159 }
160
161 #[must_use]
162 pub const fn kind(&self) -> &JsoncErrorKind {
163 &self.kind
164 }
165}
166
167impl fmt::Display for JsoncError {
168 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169 write!(formatter, "JSONC error at byte {}: ", self.offset)?;
170 match &self.kind {
171 JsoncErrorKind::UnexpectedEnd => formatter.write_str("unexpected end of input"),
172 JsoncErrorKind::UnexpectedToken => formatter.write_str("unexpected token"),
173 JsoncErrorKind::UnterminatedBlockComment => {
174 formatter.write_str("unterminated block comment")
175 }
176 JsoncErrorKind::UnterminatedString => formatter.write_str("unterminated string"),
177 JsoncErrorKind::InvalidEscape => formatter.write_str("invalid string escape"),
178 JsoncErrorKind::InvalidUnicodeEscape => formatter.write_str("invalid Unicode escape"),
179 JsoncErrorKind::LoneSurrogate => formatter.write_str("lone UTF-16 surrogate"),
180 JsoncErrorKind::UnescapedControlCharacter => {
181 formatter.write_str("unescaped control character in string")
182 }
183 JsoncErrorKind::InvalidNumber => formatter.write_str("invalid number"),
184 JsoncErrorKind::DuplicateObjectKey { key } => {
185 write!(formatter, "duplicate object key {key:?}")
186 }
187 JsoncErrorKind::TrailingCharacters => {
188 formatter.write_str("characters follow the root value")
189 }
190 JsoncErrorKind::NestingTooDeep => formatter.write_str("nesting limit exceeded"),
191 }
192 }
193}
194
195impl std::error::Error for JsoncError {}
196
197#[derive(Clone, Debug, Eq, PartialEq)]
199pub struct JsonObject {
200 entries: Arc<[(Arc<str>, JsonValue)]>,
201}
202
203impl JsonObject {
204 #[must_use]
205 pub fn get(&self, key: &str) -> Option<&JsonValue> {
206 self.entries
207 .iter()
208 .find_map(|(candidate, value)| (candidate.as_ref() == key).then_some(value))
209 }
210
211 #[must_use]
212 pub fn entries(&self) -> &[(Arc<str>, JsonValue)] {
213 &self.entries
214 }
215}
216
217#[derive(Clone, Debug, Eq, PartialEq)]
219pub enum JsonValue {
220 Null,
221 Bool(bool),
222 Number(Arc<str>),
223 String(Arc<str>),
224 Array(Arc<[JsonValue]>),
225 Object(JsonObject),
226}
227
228impl JsonValue {
229 #[must_use]
230 pub const fn as_object(&self) -> Option<&JsonObject> {
231 if let Self::Object(value) = self {
232 Some(value)
233 } else {
234 None
235 }
236 }
237
238 #[must_use]
239 pub fn as_str(&self) -> Option<&str> {
240 if let Self::String(value) = self {
241 Some(value)
242 } else {
243 None
244 }
245 }
246
247 #[must_use]
248 pub const fn as_bool(&self) -> Option<bool> {
249 if let Self::Bool(value) = self {
250 Some(*value)
251 } else {
252 None
253 }
254 }
255
256 #[must_use]
257 pub fn as_array(&self) -> Option<&[JsonValue]> {
258 if let Self::Array(value) = self {
259 Some(value)
260 } else {
261 None
262 }
263 }
264}
265
266pub fn parse_jsonc(source: &str) -> Result<JsonValue, JsoncError> {
269 JsoncParser::new(source).parse()
270}
271
272struct JsoncParser<'a> {
273 source: &'a str,
274 bytes: &'a [u8],
275 position: usize,
276}
277
278impl<'a> JsoncParser<'a> {
279 const fn new(source: &'a str) -> Self {
280 Self {
281 source,
282 bytes: source.as_bytes(),
283 position: 0,
284 }
285 }
286
287 fn parse(mut self) -> Result<JsonValue, JsoncError> {
288 self.skip_trivia()?;
289 let value = self.parse_value(0)?;
290 self.skip_trivia()?;
291 if self.position != self.bytes.len() {
292 return self.error(JsoncErrorKind::TrailingCharacters);
293 }
294 Ok(value)
295 }
296
297 fn parse_value(&mut self, depth: usize) -> Result<JsonValue, JsoncError> {
298 if depth > MAX_JSON_DEPTH {
299 return self.error(JsoncErrorKind::NestingTooDeep);
300 }
301 self.skip_trivia()?;
302 match self.peek() {
303 Some(b'{') => self.parse_object(depth + 1),
304 Some(b'[') => self.parse_array(depth + 1),
305 Some(b'"') => self.parse_string().map(JsonValue::String),
306 Some(b't') => {
307 self.expect_literal(b"true")?;
308 Ok(JsonValue::Bool(true))
309 }
310 Some(b'f') => {
311 self.expect_literal(b"false")?;
312 Ok(JsonValue::Bool(false))
313 }
314 Some(b'n') => {
315 self.expect_literal(b"null")?;
316 Ok(JsonValue::Null)
317 }
318 Some(b'-' | b'0'..=b'9') => self.parse_number().map(JsonValue::Number),
319 Some(_) => self.error(JsoncErrorKind::UnexpectedToken),
320 None => self.error(JsoncErrorKind::UnexpectedEnd),
321 }
322 }
323
324 fn parse_object(&mut self, depth: usize) -> Result<JsonValue, JsoncError> {
325 self.position += 1;
326 self.skip_trivia()?;
327 let mut entries: Vec<(Arc<str>, JsonValue)> = Vec::new();
328 if self.consume(b'}') {
329 return Ok(JsonValue::Object(JsonObject {
330 entries: Arc::from(entries),
331 }));
332 }
333 loop {
334 self.skip_trivia()?;
335 if self.peek() != Some(b'"') {
336 return self.error(JsoncErrorKind::UnexpectedToken);
337 }
338 let key_offset = self.position;
339 let key = self.parse_string()?;
340 if entries
341 .iter()
342 .any(|(existing, _)| existing.as_ref() == key.as_ref())
343 {
344 return Err(JsoncError {
345 offset: key_offset,
346 kind: JsoncErrorKind::DuplicateObjectKey { key },
347 });
348 }
349 self.skip_trivia()?;
350 if !self.consume(b':') {
351 return self.error(JsoncErrorKind::UnexpectedToken);
352 }
353 let value = self.parse_value(depth)?;
354 entries.push((key, value));
355 self.skip_trivia()?;
356 if self.consume(b'}') {
357 break;
358 }
359 if !self.consume(b',') {
360 return self.error(JsoncErrorKind::UnexpectedToken);
361 }
362 self.skip_trivia()?;
363 if self.consume(b'}') {
364 break;
365 }
366 }
367 Ok(JsonValue::Object(JsonObject {
368 entries: Arc::from(entries),
369 }))
370 }
371
372 fn parse_array(&mut self, depth: usize) -> Result<JsonValue, JsoncError> {
373 self.position += 1;
374 self.skip_trivia()?;
375 let mut values = Vec::new();
376 if self.consume(b']') {
377 return Ok(JsonValue::Array(Arc::from(values)));
378 }
379 loop {
380 values.push(self.parse_value(depth)?);
381 self.skip_trivia()?;
382 if self.consume(b']') {
383 break;
384 }
385 if !self.consume(b',') {
386 return self.error(JsoncErrorKind::UnexpectedToken);
387 }
388 self.skip_trivia()?;
389 if self.consume(b']') {
390 break;
391 }
392 }
393 Ok(JsonValue::Array(Arc::from(values)))
394 }
395
396 fn parse_string(&mut self) -> Result<Arc<str>, JsoncError> {
397 let opening = self.position;
398 self.position += 1;
399 let content_start = self.position;
400 let mut decoded: Option<String> = None;
401 let mut segment_start = content_start;
402 while let Some(byte) = self.peek() {
403 match byte {
404 b'"' => {
405 let end = self.position;
406 self.position += 1;
407 if let Some(mut text) = decoded {
408 text.push_str(&self.source[segment_start..end]);
409 return Ok(Arc::from(text));
410 }
411 return Ok(Arc::from(&self.source[content_start..end]));
412 }
413 b'\\' => {
414 let escape_start = self.position;
415 let text = decoded.get_or_insert_with(String::new);
416 text.push_str(&self.source[segment_start..escape_start]);
417 self.position += 1;
418 let escape = self.peek().ok_or(JsoncError {
419 offset: opening,
420 kind: JsoncErrorKind::UnterminatedString,
421 })?;
422 self.position += 1;
423 match escape {
424 b'"' => text.push('"'),
425 b'\\' => text.push('\\'),
426 b'/' => text.push('/'),
427 b'b' => text.push('\u{0008}'),
428 b'f' => text.push('\u{000c}'),
429 b'n' => text.push('\n'),
430 b'r' => text.push('\r'),
431 b't' => text.push('\t'),
432 b'u' => {
433 let first_offset = self.position;
434 let first = self.parse_hex_quad()?;
435 let scalar = if (0xd800..=0xdbff).contains(&first) {
436 if self.bytes.get(self.position..self.position + 2) != Some(b"\\u")
437 {
438 return Err(JsoncError {
439 offset: first_offset,
440 kind: JsoncErrorKind::LoneSurrogate,
441 });
442 }
443 self.position += 2;
444 let second_offset = self.position;
445 let second = self.parse_hex_quad()?;
446 if !(0xdc00..=0xdfff).contains(&second) {
447 return Err(JsoncError {
448 offset: second_offset,
449 kind: JsoncErrorKind::LoneSurrogate,
450 });
451 }
452 0x1_0000
453 + ((u32::from(first) - 0xd800) << 10)
454 + (u32::from(second) - 0xdc00)
455 } else if (0xdc00..=0xdfff).contains(&first) {
456 return Err(JsoncError {
457 offset: first_offset,
458 kind: JsoncErrorKind::LoneSurrogate,
459 });
460 } else {
461 u32::from(first)
462 };
463 let character = char::from_u32(scalar).ok_or(JsoncError {
464 offset: first_offset,
465 kind: JsoncErrorKind::InvalidUnicodeEscape,
466 })?;
467 text.push(character);
468 }
469 _ => {
470 return Err(JsoncError {
471 offset: escape_start,
472 kind: JsoncErrorKind::InvalidEscape,
473 });
474 }
475 }
476 segment_start = self.position;
477 }
478 0x00..=0x1f => {
479 return self.error(JsoncErrorKind::UnescapedControlCharacter);
480 }
481 _ => {
482 let character =
483 self.source[self.position..]
484 .chars()
485 .next()
486 .ok_or(JsoncError {
487 offset: opening,
488 kind: JsoncErrorKind::UnterminatedString,
489 })?;
490 self.position += character.len_utf8();
491 }
492 }
493 }
494 Err(JsoncError {
495 offset: opening,
496 kind: JsoncErrorKind::UnterminatedString,
497 })
498 }
499
500 fn parse_hex_quad(&mut self) -> Result<u16, JsoncError> {
501 let start = self.position;
502 let end = start.saturating_add(4);
503 let digits = self.bytes.get(start..end).ok_or(JsoncError {
504 offset: start,
505 kind: JsoncErrorKind::InvalidUnicodeEscape,
506 })?;
507 let mut value = 0_u16;
508 for &digit in digits {
509 value = value
510 .checked_mul(16)
511 .and_then(|prefix| hex_value(digit).map(|suffix| prefix + suffix))
512 .ok_or(JsoncError {
513 offset: start,
514 kind: JsoncErrorKind::InvalidUnicodeEscape,
515 })?;
516 }
517 self.position = end;
518 Ok(value)
519 }
520
521 fn parse_number(&mut self) -> Result<Arc<str>, JsoncError> {
522 let start = self.position;
523 self.consume(b'-');
524 match self.peek() {
525 Some(b'0') => {
526 self.position += 1;
527 if matches!(self.peek(), Some(b'0'..=b'9')) {
528 return self.error(JsoncErrorKind::InvalidNumber);
529 }
530 }
531 Some(b'1'..=b'9') => {
532 self.position += 1;
533 while matches!(self.peek(), Some(b'0'..=b'9')) {
534 self.position += 1;
535 }
536 }
537 _ => return self.error(JsoncErrorKind::InvalidNumber),
538 }
539 if self.consume(b'.') {
540 if !matches!(self.peek(), Some(b'0'..=b'9')) {
541 return self.error(JsoncErrorKind::InvalidNumber);
542 }
543 while matches!(self.peek(), Some(b'0'..=b'9')) {
544 self.position += 1;
545 }
546 }
547 if matches!(self.peek(), Some(b'e' | b'E')) {
548 self.position += 1;
549 if matches!(self.peek(), Some(b'+' | b'-')) {
550 self.position += 1;
551 }
552 if !matches!(self.peek(), Some(b'0'..=b'9')) {
553 return self.error(JsoncErrorKind::InvalidNumber);
554 }
555 while matches!(self.peek(), Some(b'0'..=b'9')) {
556 self.position += 1;
557 }
558 }
559 Ok(Arc::from(&self.source[start..self.position]))
560 }
561
562 fn expect_literal(&mut self, literal: &[u8]) -> Result<(), JsoncError> {
563 if self.bytes.get(self.position..self.position + literal.len()) == Some(literal) {
564 self.position += literal.len();
565 Ok(())
566 } else {
567 self.error(JsoncErrorKind::UnexpectedToken)
568 }
569 }
570
571 fn skip_trivia(&mut self) -> Result<(), JsoncError> {
572 loop {
573 while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
574 self.position += 1;
575 }
576 match self.bytes.get(self.position..self.position + 2) {
577 Some(b"//") => {
578 self.position += 2;
579 while !matches!(self.peek(), None | Some(b'\r' | b'\n')) {
580 self.position += 1;
581 }
582 }
583 Some(b"/*") => {
584 let start = self.position;
585 self.position += 2;
586 while self.bytes.get(self.position..self.position + 2) != Some(b"*/") {
587 if self.position == self.bytes.len() {
588 return Err(JsoncError {
589 offset: start,
590 kind: JsoncErrorKind::UnterminatedBlockComment,
591 });
592 }
593 self.position += 1;
594 }
595 self.position += 2;
596 }
597 _ => return Ok(()),
598 }
599 }
600 }
601
602 fn peek(&self) -> Option<u8> {
603 self.bytes.get(self.position).copied()
604 }
605
606 fn consume(&mut self, expected: u8) -> bool {
607 if self.peek() == Some(expected) {
608 self.position += 1;
609 true
610 } else {
611 false
612 }
613 }
614
615 fn error<T>(&self, kind: JsoncErrorKind) -> Result<T, JsoncError> {
616 Err(JsoncError {
617 offset: self.position,
618 kind,
619 })
620 }
621}
622
623const fn hex_value(byte: u8) -> Option<u16> {
624 match byte {
625 b'0'..=b'9' => Some((byte - b'0') as u16),
626 b'a'..=b'f' => Some((byte - b'a' + 10) as u16),
627 b'A'..=b'F' => Some((byte - b'A' + 10) as u16),
628 _ => None,
629 }
630}
631
632#[derive(Clone, Debug, Eq, PartialEq)]
634pub struct BamtsTomlError {
635 line: usize,
636 message: Arc<str>,
637}
638
639impl BamtsTomlError {
640 #[must_use]
641 pub const fn line(&self) -> usize {
642 self.line
643 }
644
645 #[must_use]
646 pub fn message(&self) -> &str {
647 &self.message
648 }
649}
650
651impl fmt::Display for BamtsTomlError {
652 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
653 write!(formatter, "bamts.toml:{}: {}", self.line, self.message)
654 }
655}
656
657impl std::error::Error for BamtsTomlError {}
658
659#[derive(Clone, Copy, Debug, Eq, PartialEq)]
660enum LintTomlSection {
661 Other,
662 Groups,
663 Rules,
664}
665
666pub fn parse_bamts_toml(source: &str) -> Result<LintConfig, BamtsTomlError> {
669 let mut section = LintTomlSection::Other;
670 let mut groups = Vec::new();
671 let mut rules = Vec::new();
672 for (line_index, raw_line) in source.lines().enumerate() {
673 let line_number = line_index + 1;
674 let line = strip_toml_comment(raw_line).trim();
675 if line.is_empty() {
676 continue;
677 }
678 if line.starts_with('[') {
679 let Some(name) = line
680 .strip_prefix('[')
681 .and_then(|line| line.strip_suffix(']'))
682 else {
683 return Err(bamts_toml_error(line_number, "malformed table header"));
684 };
685 section = match name.trim() {
686 "lints.groups" => LintTomlSection::Groups,
687 "lints.rules" => LintTomlSection::Rules,
688 _ => LintTomlSection::Other,
689 };
690 continue;
691 }
692 if section == LintTomlSection::Other {
693 continue;
694 }
695 let Some((raw_name, raw_level)) = line.split_once('=') else {
696 return Err(bamts_toml_error(
697 line_number,
698 "lint setting must be `name = \"level\"`",
699 ));
700 };
701 let name = parse_toml_atom(raw_name.trim()).ok_or_else(|| {
702 bamts_toml_error(
703 line_number,
704 "lint name must be a non-empty bare or quoted key",
705 )
706 })?;
707 let level_name = parse_toml_atom(raw_level.trim())
708 .ok_or_else(|| bamts_toml_error(line_number, "lint level must be a quoted string"))?;
709 if !raw_level.trim().starts_with('"') {
710 return Err(bamts_toml_error(
711 line_number,
712 "lint level must be a quoted string",
713 ));
714 }
715 let level = level_name.parse::<LintLevel>().map_err(|_| {
716 bamts_toml_error(
717 line_number,
718 "lint level must be one of allow, warn, deny, or forbid",
719 )
720 })?;
721 let setting = LintSetting::new(name, level, format!("bamts.toml:{line_number}"));
722 match section {
723 LintTomlSection::Groups => groups.push(setting),
724 LintTomlSection::Rules => rules.push(setting),
725 LintTomlSection::Other => unreachable!("other sections were skipped"),
726 }
727 }
728 Ok(LintConfig::new(groups, rules))
729}
730
731fn strip_toml_comment(line: &str) -> &str {
732 let mut quoted = false;
733 let mut escaped = false;
734 for (index, character) in line.char_indices() {
735 if escaped {
736 escaped = false;
737 continue;
738 }
739 match character {
740 '\\' if quoted => escaped = true,
741 '"' => quoted = !quoted,
742 '#' if !quoted => return &line[..index],
743 _ => {}
744 }
745 }
746 line
747}
748
749fn parse_toml_atom(value: &str) -> Option<&str> {
750 if let Some(quoted) = value
751 .strip_prefix('"')
752 .and_then(|value| value.strip_suffix('"'))
753 {
754 (!quoted.is_empty() && !quoted.contains(['"', '\\'])).then_some(quoted)
755 } else {
756 (!value.is_empty()
757 && value
758 .bytes()
759 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')))
760 .then_some(value)
761 }
762}
763
764fn bamts_toml_error(line: usize, message: &'static str) -> BamtsTomlError {
765 BamtsTomlError {
766 line,
767 message: Arc::from(message),
768 }
769}
770
771#[derive(Clone, Debug, Eq, PartialEq)]
773pub enum ConfigError {
774 Json(JsoncError),
775 Path(PathError),
776 RootMustBeObject,
777 InvalidField {
778 field: Arc<str>,
779 expected: &'static str,
780 },
781}
782
783impl fmt::Display for ConfigError {
784 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
785 match self {
786 Self::Json(error) => error.fmt(formatter),
787 Self::Path(error) => error.fmt(formatter),
788 Self::RootMustBeObject => formatter.write_str("tsconfig root must be an object"),
789 Self::InvalidField { field, expected } => {
790 write!(formatter, "tsconfig field {field:?} must be {expected}")
791 }
792 }
793 }
794}
795
796impl std::error::Error for ConfigError {
797 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
798 match self {
799 Self::Json(error) => Some(error),
800 Self::Path(error) => Some(error),
801 Self::RootMustBeObject | Self::InvalidField { .. } => None,
802 }
803 }
804}
805
806impl From<JsoncError> for ConfigError {
807 fn from(error: JsoncError) -> Self {
808 Self::Json(error)
809 }
810}
811
812impl From<PathError> for ConfigError {
813 fn from(error: PathError) -> Self {
814 Self::Path(error)
815 }
816}
817
818#[derive(Clone, Debug, Eq, PartialEq)]
821pub struct PathMapping {
822 pattern: Arc<str>,
823 targets: Arc<[PathBuf]>,
824}
825
826impl PathMapping {
827 #[must_use]
828 pub fn pattern(&self) -> &str {
829 &self.pattern
830 }
831
832 #[must_use]
833 pub fn targets(&self) -> &[PathBuf] {
834 &self.targets
835 }
836}
837#[derive(Clone, Debug, Eq, PartialEq)]
842pub struct LintTsConfig {
843 target: Option<Arc<str>>,
844 module: Option<Arc<str>>,
845 module_resolution: Option<Arc<str>>,
846 paths: Arc<[PathMapping]>,
847}
848
849impl LintTsConfig {
850 pub fn parse(
852 root: &ProjectRoot,
853 config_path: impl AsRef<Path>,
854 source: &str,
855 ) -> Result<Self, ConfigError> {
856 let path = root.confine(config_path)?;
857 let directory = path
858 .parent()
859 .ok_or_else(|| PathError::PathHasNoParent { path: path.clone() })?;
860 let raw = parse_jsonc(source)?
861 .as_object()
862 .ok_or(ConfigError::RootMustBeObject)?
863 .clone();
864 let compiler = match raw.get("compilerOptions") {
865 None => None,
866 Some(value) => Some(value.as_object().ok_or_else(|| ConfigError::InvalidField {
867 field: Arc::from("compilerOptions"),
868 expected: "an object",
869 })?),
870 };
871 Ok(Self {
872 target: optional_nested_string(compiler, "target")?,
873 module: optional_nested_string(compiler, "module")?,
874 module_resolution: optional_nested_string(compiler, "moduleResolution")?,
875 paths: parse_path_mappings(root, directory, compiler)?,
876 })
877 }
878
879 #[must_use]
880 pub fn target(&self) -> Option<&str> {
881 self.target.as_deref()
882 }
883
884 #[must_use]
885 pub fn module(&self) -> Option<&str> {
886 self.module.as_deref()
887 }
888
889 #[must_use]
890 pub fn module_resolution(&self) -> Option<&str> {
891 self.module_resolution.as_deref()
892 }
893
894 #[must_use]
895 pub fn paths(&self) -> &[PathMapping] {
896 &self.paths
897 }
898}
899
900#[derive(Clone, Debug, Eq, PartialEq)]
904pub struct CompilerOptions {
905 target: Option<Arc<str>>,
906 module: Option<Arc<str>>,
907 module_resolution: Option<Arc<str>>,
908 jsx: Option<Arc<str>>,
909 strict: bool,
910 allow_js: bool,
911 check_js: bool,
912 resolve_json_module: bool,
913 base_url: PathBuf,
914 root_dir: Option<PathBuf>,
915 out_dir: Option<PathBuf>,
916 paths: Arc<[PathMapping]>,
917}
918
919impl CompilerOptions {
920 #[must_use]
921 pub fn target(&self) -> Option<&str> {
922 self.target.as_deref()
923 }
924
925 #[must_use]
926 pub fn module(&self) -> Option<&str> {
927 self.module.as_deref()
928 }
929
930 #[must_use]
931 pub fn module_resolution(&self) -> Option<&str> {
932 self.module_resolution.as_deref()
933 }
934
935 #[must_use]
936 pub fn jsx(&self) -> Option<&str> {
937 self.jsx.as_deref()
938 }
939
940 #[must_use]
941 pub const fn strict(&self) -> bool {
942 self.strict
943 }
944
945 #[must_use]
946 pub const fn allow_js(&self) -> bool {
947 self.allow_js
948 }
949
950 #[must_use]
951 pub const fn check_js(&self) -> bool {
952 self.check_js
953 }
954
955 #[must_use]
956 pub const fn resolve_json_module(&self) -> bool {
957 self.resolve_json_module
958 }
959
960 #[must_use]
961 pub fn base_url(&self) -> &Path {
962 &self.base_url
963 }
964
965 #[must_use]
966 pub fn root_dir(&self) -> Option<&Path> {
967 self.root_dir.as_deref()
968 }
969
970 #[must_use]
971 pub fn out_dir(&self) -> Option<&Path> {
972 self.out_dir.as_deref()
973 }
974
975 #[must_use]
976 pub fn paths(&self) -> &[PathMapping] {
977 &self.paths
978 }
979}
980
981#[derive(Clone, Debug, Eq, PartialEq)]
984pub struct ProjectConfig {
985 path: PathBuf,
986 extends: Option<Arc<str>>,
987 files: Arc<[PathBuf]>,
988 include: Arc<[Arc<str>]>,
989 exclude: Arc<[Arc<str>]>,
990 options: CompilerOptions,
991 raw: JsonObject,
992}
993
994impl ProjectConfig {
995 pub fn parse(
997 root: &ProjectRoot,
998 config_path: impl AsRef<Path>,
999 source: &str,
1000 ) -> Result<Self, ConfigError> {
1001 let path = root.confine(config_path)?;
1002 let directory = path
1003 .parent()
1004 .ok_or_else(|| PathError::PathHasNoParent { path: path.clone() })?;
1005 let value = parse_jsonc(source)?;
1006 let raw = value
1007 .as_object()
1008 .ok_or(ConfigError::RootMustBeObject)?
1009 .clone();
1010 let extends = optional_string(&raw, "extends")?;
1011 let files = path_list(root, directory, &raw, "files")?;
1012 let include = string_list(&raw, "include")?;
1013 let exclude = string_list(&raw, "exclude")?;
1014 validate_patterns(root, directory, "include", &include)?;
1015 validate_patterns(root, directory, "exclude", &exclude)?;
1016
1017 let compiler = match raw.get("compilerOptions") {
1018 None => None,
1019 Some(value) => Some(value.as_object().ok_or_else(|| ConfigError::InvalidField {
1020 field: Arc::from("compilerOptions"),
1021 expected: "an object",
1022 })?),
1023 };
1024 let base_url = optional_path(root, directory, compiler, "baseUrl")?
1025 .unwrap_or_else(|| directory.to_path_buf());
1026 let paths = parse_path_mappings(root, &base_url, compiler)?;
1027 let options = CompilerOptions {
1028 target: optional_nested_string(compiler, "target")?,
1029 module: optional_nested_string(compiler, "module")?,
1030 module_resolution: optional_nested_string(compiler, "moduleResolution")?,
1031 jsx: optional_nested_string(compiler, "jsx")?,
1032 strict: optional_bool(compiler, "strict")?.unwrap_or(false),
1033 allow_js: optional_bool(compiler, "allowJs")?.unwrap_or(false),
1034 check_js: optional_bool(compiler, "checkJs")?.unwrap_or(false),
1035 resolve_json_module: optional_bool(compiler, "resolveJsonModule")?.unwrap_or(false),
1036 base_url,
1037 root_dir: optional_path(root, directory, compiler, "rootDir")?,
1038 out_dir: optional_path(root, directory, compiler, "outDir")?,
1039 paths,
1040 };
1041 Ok(Self {
1042 path,
1043 extends,
1044 files,
1045 include,
1046 exclude,
1047 options,
1048 raw,
1049 })
1050 }
1051
1052 #[must_use]
1053 pub fn path(&self) -> &Path {
1054 &self.path
1055 }
1056
1057 #[must_use]
1058 pub fn extends(&self) -> Option<&str> {
1059 self.extends.as_deref()
1060 }
1061
1062 #[must_use]
1063 pub fn files(&self) -> &[PathBuf] {
1064 &self.files
1065 }
1066
1067 #[must_use]
1068 pub fn include(&self) -> &[Arc<str>] {
1069 &self.include
1070 }
1071
1072 #[must_use]
1073 pub fn exclude(&self) -> &[Arc<str>] {
1074 &self.exclude
1075 }
1076
1077 #[must_use]
1078 pub const fn options(&self) -> &CompilerOptions {
1079 &self.options
1080 }
1081
1082 #[must_use]
1083 pub const fn raw(&self) -> &JsonObject {
1084 &self.raw
1085 }
1086}
1087
1088fn invalid_field(field: impl Into<Arc<str>>, expected: &'static str) -> ConfigError {
1089 ConfigError::InvalidField {
1090 field: field.into(),
1091 expected,
1092 }
1093}
1094
1095fn optional_string(
1096 object: &JsonObject,
1097 key: &'static str,
1098) -> Result<Option<Arc<str>>, ConfigError> {
1099 object
1100 .get(key)
1101 .map(|value| {
1102 value
1103 .as_str()
1104 .map(Arc::from)
1105 .ok_or_else(|| invalid_field(key, "a string"))
1106 })
1107 .transpose()
1108}
1109
1110fn optional_nested_string(
1111 object: Option<&JsonObject>,
1112 key: &'static str,
1113) -> Result<Option<Arc<str>>, ConfigError> {
1114 object.map_or(Ok(None), |object| optional_string(object, key))
1115}
1116
1117fn optional_bool(
1118 object: Option<&JsonObject>,
1119 key: &'static str,
1120) -> Result<Option<bool>, ConfigError> {
1121 object
1122 .and_then(|object| object.get(key))
1123 .map(|value| {
1124 value
1125 .as_bool()
1126 .ok_or_else(|| invalid_field(key, "a boolean"))
1127 })
1128 .transpose()
1129}
1130
1131fn optional_path(
1132 root: &ProjectRoot,
1133 directory: &Path,
1134 object: Option<&JsonObject>,
1135 key: &'static str,
1136) -> Result<Option<PathBuf>, ConfigError> {
1137 object
1138 .and_then(|object| object.get(key))
1139 .map(|value| {
1140 let value = value
1141 .as_str()
1142 .ok_or_else(|| invalid_field(key, "a path string"))?;
1143 root.resolve_from(directory, value)
1144 .map_err(ConfigError::from)
1145 })
1146 .transpose()
1147}
1148
1149fn string_list(object: &JsonObject, key: &'static str) -> Result<Arc<[Arc<str>]>, ConfigError> {
1150 let Some(value) = object.get(key) else {
1151 return Ok(Arc::from([]));
1152 };
1153 let values = value
1154 .as_array()
1155 .ok_or_else(|| invalid_field(key, "an array of strings"))?;
1156 values
1157 .iter()
1158 .map(|value| {
1159 value
1160 .as_str()
1161 .map(Arc::from)
1162 .ok_or_else(|| invalid_field(key, "an array of strings"))
1163 })
1164 .collect::<Result<Vec<_>, _>>()
1165 .map(Arc::from)
1166}
1167
1168fn path_list(
1169 root: &ProjectRoot,
1170 directory: &Path,
1171 object: &JsonObject,
1172 key: &'static str,
1173) -> Result<Arc<[PathBuf]>, ConfigError> {
1174 let values = string_list(object, key)?;
1175 values
1176 .iter()
1177 .map(|value| {
1178 root.resolve_from(directory, value.as_ref())
1179 .map_err(ConfigError::from)
1180 })
1181 .collect::<Result<Vec<_>, _>>()
1182 .map(Arc::from)
1183}
1184
1185fn validate_patterns(
1186 root: &ProjectRoot,
1187 directory: &Path,
1188 field: &'static str,
1189 patterns: &[Arc<str>],
1190) -> Result<(), ConfigError> {
1191 for pattern in patterns {
1192 if pattern.is_empty() {
1193 return Err(invalid_field(field, "non-empty confined path patterns"));
1194 }
1195 root.resolve_from(directory, pattern.as_ref())?;
1196 }
1197 Ok(())
1198}
1199
1200fn parse_path_mappings(
1201 root: &ProjectRoot,
1202 base_url: &Path,
1203 compiler: Option<&JsonObject>,
1204) -> Result<Arc<[PathMapping]>, ConfigError> {
1205 let Some(value) = compiler.and_then(|object| object.get("paths")) else {
1206 return Ok(Arc::from([]));
1207 };
1208 let object = value
1209 .as_object()
1210 .ok_or_else(|| invalid_field("paths", "an object of string arrays"))?;
1211 let mut mappings = Vec::with_capacity(object.entries().len());
1212 for (pattern, value) in object.entries() {
1213 if pattern.is_empty() || pattern.matches('*').count() > 1 {
1214 return Err(invalid_field(
1215 format!("paths.{pattern}"),
1216 "a non-empty pattern with at most one '*'",
1217 ));
1218 }
1219 let targets = value
1220 .as_array()
1221 .ok_or_else(|| invalid_field(format!("paths.{pattern}"), "an array of strings"))?;
1222 if targets.is_empty() {
1223 return Err(invalid_field(
1224 format!("paths.{pattern}"),
1225 "a non-empty array of strings",
1226 ));
1227 }
1228 let resolved = targets
1229 .iter()
1230 .map(|target| {
1231 let target = target.as_str().ok_or_else(|| {
1232 invalid_field(format!("paths.{pattern}"), "an array of strings")
1233 })?;
1234 if target.matches('*').count() > 1 {
1235 return Err(invalid_field(
1236 format!("paths.{pattern}"),
1237 "targets with at most one '*'",
1238 ));
1239 }
1240 root.resolve_from(base_url, target)
1241 .map_err(ConfigError::from)
1242 })
1243 .collect::<Result<Vec<_>, _>>()?;
1244 mappings.push(PathMapping {
1245 pattern: Arc::clone(pattern),
1246 targets: Arc::from(resolved),
1247 });
1248 }
1249 Ok(Arc::from(mappings))
1250}
1251
1252#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1254pub enum ResolutionFlavor {
1255 Runtime,
1256 Types,
1257}
1258
1259#[derive(Clone, Debug, Eq, PartialEq)]
1262pub enum ModuleResolutionError {
1263 Path(PathError),
1264 EmptySpecifier,
1265 BareSpecifier { specifier: Arc<str> },
1266 UrlLikeSpecifier { specifier: Arc<str> },
1267 UnsupportedExtension { specifier: Arc<str> },
1268}
1269
1270impl fmt::Display for ModuleResolutionError {
1271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1272 match self {
1273 Self::Path(error) => error.fmt(formatter),
1274 Self::EmptySpecifier => formatter.write_str("module specifier is empty"),
1275 Self::BareSpecifier { specifier } => {
1276 write!(
1277 formatter,
1278 "{specifier:?} is not a relative module specifier"
1279 )
1280 }
1281 Self::UrlLikeSpecifier { specifier } => write!(
1282 formatter,
1283 "URL-like module specifier {specifier:?} cannot be resolved as a file"
1284 ),
1285 Self::UnsupportedExtension { specifier } => write!(
1286 formatter,
1287 "relative module specifier {specifier:?} has an unsupported extension"
1288 ),
1289 }
1290 }
1291}
1292
1293impl std::error::Error for ModuleResolutionError {
1294 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1295 if let Self::Path(error) = self {
1296 Some(error)
1297 } else {
1298 None
1299 }
1300 }
1301}
1302
1303impl From<PathError> for ModuleResolutionError {
1304 fn from(error: PathError) -> Self {
1305 Self::Path(error)
1306 }
1307}
1308
1309#[derive(Clone, Debug, Eq, PartialEq)]
1311pub struct ResolutionPlan {
1312 specifier: Arc<str>,
1313 candidates: Arc<[PathBuf]>,
1314}
1315
1316impl ResolutionPlan {
1317 #[must_use]
1318 pub fn specifier(&self) -> &str {
1319 &self.specifier
1320 }
1321
1322 #[must_use]
1323 pub fn candidates(&self) -> &[PathBuf] {
1324 &self.candidates
1325 }
1326
1327 #[must_use]
1330 pub fn select(&self, mut exists: impl FnMut(&Path) -> bool) -> Option<&Path> {
1331 self.candidates
1332 .iter()
1333 .find(|candidate| exists(candidate))
1334 .map(PathBuf::as_path)
1335 }
1336}
1337
1338pub fn plan_relative_module(
1341 root: &ProjectRoot,
1342 importer: impl AsRef<Path>,
1343 specifier: &str,
1344 flavor: ResolutionFlavor,
1345 resolve_json_module: bool,
1346) -> Result<ResolutionPlan, ModuleResolutionError> {
1347 if specifier.is_empty() {
1348 return Err(ModuleResolutionError::EmptySpecifier);
1349 }
1350 if specifier.contains('?') || specifier.contains('#') || specifier.contains("//") {
1351 return Err(ModuleResolutionError::UrlLikeSpecifier {
1352 specifier: Arc::from(specifier),
1353 });
1354 }
1355 if !(specifier.starts_with("./") || specifier.starts_with("../")) {
1356 return Err(ModuleResolutionError::BareSpecifier {
1357 specifier: Arc::from(specifier),
1358 });
1359 }
1360 let importer = root.confine(importer)?;
1361 let directory = importer
1362 .parent()
1363 .ok_or_else(|| PathError::PathHasNoParent {
1364 path: importer.clone(),
1365 })?;
1366 let requested = root.resolve_from(directory, specifier)?;
1367 let mut candidates = Vec::new();
1368 append_file_candidates(
1369 &mut candidates,
1370 &requested,
1371 flavor,
1372 resolve_json_module,
1373 specifier,
1374 )?;
1375 if requested.extension().is_none() {
1376 let index = requested.join("index");
1377 append_extensionless_candidates(&mut candidates, &index, flavor, resolve_json_module);
1378 }
1379 Ok(ResolutionPlan {
1380 specifier: Arc::from(specifier),
1381 candidates: Arc::from(candidates),
1382 })
1383}
1384
1385fn append_file_candidates(
1386 output: &mut Vec<PathBuf>,
1387 requested: &Path,
1388 flavor: ResolutionFlavor,
1389 resolve_json_module: bool,
1390 specifier: &str,
1391) -> Result<(), ModuleResolutionError> {
1392 let Some(extension) = requested.extension().and_then(|value| value.to_str()) else {
1393 append_unique(output, requested.to_path_buf());
1394 append_extensionless_candidates(output, requested, flavor, resolve_json_module);
1395 return Ok(());
1396 };
1397 let substitutions: &[&str] = match extension {
1398 "js" => &["ts", "tsx", "d.ts", "js"],
1399 "jsx" => &["tsx", "d.ts", "jsx"],
1400 "mjs" => &["mts", "d.mts", "mjs"],
1401 "cjs" => &["cts", "d.cts", "cjs"],
1402 "ts" | "tsx" | "mts" | "cts" => &[extension],
1403 "json" if resolve_json_module => &["json"],
1404 _ => {
1405 return Err(ModuleResolutionError::UnsupportedExtension {
1406 specifier: Arc::from(specifier),
1407 });
1408 }
1409 };
1410 if flavor == ResolutionFlavor::Types {
1411 for extension in substitutions.iter().filter(|value| value.contains("d.")) {
1412 append_with_extension(output, requested, extension);
1413 }
1414 }
1415 for extension in substitutions {
1416 append_with_extension(output, requested, extension);
1417 }
1418 Ok(())
1419}
1420
1421fn append_extensionless_candidates(
1422 output: &mut Vec<PathBuf>,
1423 stem: &Path,
1424 flavor: ResolutionFlavor,
1425 resolve_json_module: bool,
1426) {
1427 let runtime = ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
1428 if flavor == ResolutionFlavor::Types {
1429 append_with_extension(output, stem, "d.ts");
1430 append_with_extension(output, stem, "d.mts");
1431 append_with_extension(output, stem, "d.cts");
1432 }
1433 for extension in runtime {
1434 append_with_extension(output, stem, extension);
1435 }
1436 if flavor == ResolutionFlavor::Runtime {
1437 append_with_extension(output, stem, "d.ts");
1438 append_with_extension(output, stem, "d.mts");
1439 append_with_extension(output, stem, "d.cts");
1440 }
1441 if resolve_json_module {
1442 append_with_extension(output, stem, "json");
1443 }
1444}
1445
1446fn append_with_extension(output: &mut Vec<PathBuf>, path: &Path, extension: &str) {
1447 let mut candidate = path.to_path_buf();
1448 candidate.set_extension(extension);
1449 append_unique(output, candidate);
1450}
1451
1452fn append_unique(output: &mut Vec<PathBuf>, candidate: PathBuf) {
1453 if !output.contains(&candidate) {
1454 output.push(candidate);
1455 }
1456}
1457
1458#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1460pub enum PackageMode {
1461 Import,
1462 Require,
1463 Types,
1464}
1465
1466#[derive(Clone, Debug, Eq, PartialEq)]
1469pub enum PackageTarget {
1470 Path(PathBuf),
1471 External(Arc<str>),
1472}
1473
1474#[derive(Clone, Debug, Eq, PartialEq)]
1476pub enum PackageError {
1477 Json(JsoncError),
1478 Path(PathError),
1479 RootMustBeObject,
1480 InvalidField {
1481 field: Arc<str>,
1482 expected: &'static str,
1483 },
1484 InvalidSubpath {
1485 subpath: Arc<str>,
1486 },
1487 InvalidImportSpecifier {
1488 specifier: Arc<str>,
1489 },
1490 InvalidCondition {
1491 condition: Arc<str>,
1492 },
1493 MixedExportsKeys,
1494 InvalidPattern {
1495 pattern: Arc<str>,
1496 },
1497 InvalidTarget {
1498 target: Arc<str>,
1499 },
1500 TargetEscapesPackage {
1501 target: Arc<str>,
1502 },
1503 TargetUsesNodeModules {
1504 target: Arc<str>,
1505 },
1506 SubpathNotExported {
1507 subpath: Arc<str>,
1508 },
1509 ImportNotDefined {
1510 specifier: Arc<str>,
1511 },
1512 TargetBlocked,
1513 NoLegacyEntry,
1514}
1515
1516impl fmt::Display for PackageError {
1517 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1518 match self {
1519 Self::Json(error) => error.fmt(formatter),
1520 Self::Path(error) => error.fmt(formatter),
1521 Self::RootMustBeObject => formatter.write_str("package.json root must be an object"),
1522 Self::InvalidField { field, expected } => {
1523 write!(formatter, "package.json field {field:?} must be {expected}")
1524 }
1525 Self::InvalidSubpath { subpath } => {
1526 write!(formatter, "invalid package export subpath {subpath:?}")
1527 }
1528 Self::InvalidImportSpecifier { specifier } => {
1529 write!(formatter, "invalid package import specifier {specifier:?}")
1530 }
1531 Self::InvalidCondition { condition } => {
1532 write!(formatter, "invalid package condition {condition:?}")
1533 }
1534 Self::MixedExportsKeys => formatter
1535 .write_str("package exports object cannot mix subpath keys and condition keys"),
1536 Self::InvalidPattern { pattern } => {
1537 write!(formatter, "invalid package map pattern {pattern:?}")
1538 }
1539 Self::InvalidTarget { target } => {
1540 write!(formatter, "invalid package target {target:?}")
1541 }
1542 Self::TargetEscapesPackage { target } => {
1543 write!(formatter, "package target {target:?} escapes its package")
1544 }
1545 Self::TargetUsesNodeModules { target } => write!(
1546 formatter,
1547 "package target {target:?} contains a forbidden node_modules segment"
1548 ),
1549 Self::SubpathNotExported { subpath } => {
1550 write!(formatter, "package subpath {subpath:?} is not exported")
1551 }
1552 Self::ImportNotDefined { specifier } => {
1553 write!(formatter, "package import {specifier:?} is not defined")
1554 }
1555 Self::TargetBlocked => formatter.write_str("package target is explicitly blocked"),
1556 Self::NoLegacyEntry => formatter.write_str("package has no matching legacy entry"),
1557 }
1558 }
1559}
1560
1561impl std::error::Error for PackageError {
1562 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1563 match self {
1564 Self::Json(error) => Some(error),
1565 Self::Path(error) => Some(error),
1566 _ => None,
1567 }
1568 }
1569}
1570
1571impl From<JsoncError> for PackageError {
1572 fn from(error: JsoncError) -> Self {
1573 Self::Json(error)
1574 }
1575}
1576
1577impl From<PathError> for PackageError {
1578 fn from(error: PathError) -> Self {
1579 Self::Path(error)
1580 }
1581}
1582
1583#[derive(Clone, Debug, Eq, PartialEq)]
1586pub struct ResolutionConditions {
1587 values: Arc<[Arc<str>]>,
1588}
1589
1590impl ResolutionConditions {
1591 pub fn new<I, S>(conditions: I) -> Result<Self, PackageError>
1592 where
1593 I: IntoIterator<Item = S>,
1594 S: AsRef<str>,
1595 {
1596 let mut values: Vec<Arc<str>> = Vec::new();
1597 for condition in conditions {
1598 let condition = condition.as_ref();
1599 if condition.is_empty() || condition.starts_with('.') || condition.contains('/') {
1600 return Err(PackageError::InvalidCondition {
1601 condition: Arc::from(condition),
1602 });
1603 }
1604 if !values.iter().any(|existing| existing.as_ref() == condition) {
1605 values.push(Arc::from(condition));
1606 }
1607 }
1608 Ok(Self {
1609 values: Arc::from(values),
1610 })
1611 }
1612
1613 #[must_use]
1614 pub fn for_mode(mode: PackageMode) -> Self {
1615 let values: &[&str] = match mode {
1616 PackageMode::Import => &["import", "node"],
1617 PackageMode::Require => &["require", "node"],
1618 PackageMode::Types => &["types", "import", "node"],
1619 };
1620 Self {
1621 values: Arc::from(
1622 values
1623 .iter()
1624 .map(|value| Arc::from(*value))
1625 .collect::<Vec<_>>(),
1626 ),
1627 }
1628 }
1629
1630 #[must_use]
1631 pub fn contains(&self, condition: &str) -> bool {
1632 condition == "default"
1633 || self
1634 .values
1635 .iter()
1636 .any(|candidate| candidate.as_ref() == condition)
1637 }
1638
1639 #[must_use]
1640 pub fn values(&self) -> &[Arc<str>] {
1641 &self.values
1642 }
1643}
1644
1645#[derive(Clone, Debug, Eq, PartialEq)]
1647pub struct PackageJson {
1648 path: PathBuf,
1649 directory: PathBuf,
1650 name: Option<Arc<str>>,
1651 package_type: Option<Arc<str>>,
1652 main: Option<Arc<str>>,
1653 module: Option<Arc<str>>,
1654 types: Option<Arc<str>>,
1655 exports: Option<JsonValue>,
1656 imports: Option<JsonObject>,
1657 raw: JsonObject,
1658}
1659
1660impl PackageJson {
1661 pub fn parse(
1663 root: &ProjectRoot,
1664 package_path: impl AsRef<Path>,
1665 source: &str,
1666 ) -> Result<Self, PackageError> {
1667 let path = root.confine(package_path)?;
1668 let directory = path
1669 .parent()
1670 .ok_or_else(|| PathError::PathHasNoParent { path: path.clone() })?
1671 .to_path_buf();
1672 let value = parse_jsonc(source)?;
1673 let raw = value
1674 .as_object()
1675 .ok_or(PackageError::RootMustBeObject)?
1676 .clone();
1677 let imports = match raw.get("imports") {
1678 None => None,
1679 Some(value) => Some(
1680 value
1681 .as_object()
1682 .ok_or_else(|| package_invalid_field("imports", "an object"))?
1683 .clone(),
1684 ),
1685 };
1686 let package_type = package_optional_string(&raw, "type")?;
1687 if let Some(value) = package_type.as_deref()
1688 && value != "module"
1689 && value != "commonjs"
1690 {
1691 return Err(package_invalid_field("type", "\"module\" or \"commonjs\""));
1692 }
1693 Ok(Self {
1694 path,
1695 directory,
1696 name: package_optional_string(&raw, "name")?,
1697 package_type,
1698 main: package_optional_string(&raw, "main")?,
1699 module: package_optional_string(&raw, "module")?,
1700 types: package_optional_string(&raw, "types")?
1701 .or(package_optional_string(&raw, "typings")?),
1702 exports: raw.get("exports").cloned(),
1703 imports,
1704 raw,
1705 })
1706 }
1707
1708 #[must_use]
1709 pub fn path(&self) -> &Path {
1710 &self.path
1711 }
1712
1713 #[must_use]
1714 pub fn directory(&self) -> &Path {
1715 &self.directory
1716 }
1717
1718 #[must_use]
1719 pub fn name(&self) -> Option<&str> {
1720 self.name.as_deref()
1721 }
1722
1723 #[must_use]
1724 pub fn package_type(&self) -> Option<&str> {
1725 self.package_type.as_deref()
1726 }
1727
1728 #[must_use]
1729 pub const fn raw(&self) -> &JsonObject {
1730 &self.raw
1731 }
1732
1733 pub fn resolve_export(
1736 &self,
1737 root: &ProjectRoot,
1738 subpath: &str,
1739 mode: PackageMode,
1740 conditions: &ResolutionConditions,
1741 ) -> Result<PathBuf, PackageError> {
1742 validate_export_subpath(subpath)?;
1743 let Some(exports) = &self.exports else {
1744 if subpath != "." {
1745 return Err(PackageError::SubpathNotExported {
1746 subpath: Arc::from(subpath),
1747 });
1748 }
1749 return self.resolve_legacy(root, mode);
1750 };
1751 let capture: Option<Arc<str>> = if let JsonValue::Object(object) = exports {
1752 let has_subpaths = object.entries().iter().any(|(key, _)| key.starts_with('.'));
1753 let has_conditions = object
1754 .entries()
1755 .iter()
1756 .any(|(key, _)| !key.starts_with('.'));
1757 if has_subpaths && has_conditions {
1758 return Err(PackageError::MixedExportsKeys);
1759 }
1760 if has_subpaths {
1761 let entry = select_map_entry(object, subpath)?.ok_or_else(|| {
1762 PackageError::SubpathNotExported {
1763 subpath: Arc::from(subpath),
1764 }
1765 })?;
1766 return self.finish_export_target(
1767 root,
1768 resolve_package_target(entry.target, conditions, entry.capture.as_deref())?,
1769 );
1770 }
1771 None
1772 } else {
1773 None
1774 };
1775 if subpath != "." {
1776 return Err(PackageError::SubpathNotExported {
1777 subpath: Arc::from(subpath),
1778 });
1779 }
1780 self.finish_export_target(
1781 root,
1782 resolve_package_target(exports, conditions, capture.as_deref())?,
1783 )
1784 }
1785
1786 pub fn resolve_import(
1789 &self,
1790 root: &ProjectRoot,
1791 specifier: &str,
1792 conditions: &ResolutionConditions,
1793 ) -> Result<PackageTarget, PackageError> {
1794 if !specifier.starts_with('#') || specifier == "#" || specifier.starts_with("#/") {
1795 return Err(PackageError::InvalidImportSpecifier {
1796 specifier: Arc::from(specifier),
1797 });
1798 }
1799 let imports = self
1800 .imports
1801 .as_ref()
1802 .ok_or_else(|| PackageError::ImportNotDefined {
1803 specifier: Arc::from(specifier),
1804 })?;
1805 let entry = select_map_entry(imports, specifier)?.ok_or_else(|| {
1806 PackageError::ImportNotDefined {
1807 specifier: Arc::from(specifier),
1808 }
1809 })?;
1810 match resolve_package_target(entry.target, conditions, entry.capture.as_deref())? {
1811 TargetOutcome::Path(target) => self
1812 .resolve_package_path(root, &target)
1813 .map(PackageTarget::Path),
1814 TargetOutcome::External(target) => Ok(PackageTarget::External(target)),
1815 TargetOutcome::Blocked => Err(PackageError::TargetBlocked),
1816 TargetOutcome::NoMatch => Err(PackageError::ImportNotDefined {
1817 specifier: Arc::from(specifier),
1818 }),
1819 }
1820 }
1821
1822 fn resolve_legacy(
1823 &self,
1824 root: &ProjectRoot,
1825 mode: PackageMode,
1826 ) -> Result<PathBuf, PackageError> {
1827 let target = match mode {
1828 PackageMode::Types => self.types.as_deref(),
1829 PackageMode::Import => self.module.as_deref().or(self.main.as_deref()),
1830 PackageMode::Require => self.main.as_deref(),
1831 }
1832 .ok_or(PackageError::NoLegacyEntry)?;
1833 self.resolve_package_path(root, target)
1834 }
1835
1836 fn finish_export_target(
1837 &self,
1838 root: &ProjectRoot,
1839 outcome: TargetOutcome,
1840 ) -> Result<PathBuf, PackageError> {
1841 match outcome {
1842 TargetOutcome::Path(target) => self.resolve_package_path(root, &target),
1843 TargetOutcome::External(target) => Err(PackageError::InvalidTarget { target }),
1844 TargetOutcome::Blocked => Err(PackageError::TargetBlocked),
1845 TargetOutcome::NoMatch => Err(PackageError::SubpathNotExported {
1846 subpath: Arc::from("."),
1847 }),
1848 }
1849 }
1850
1851 fn resolve_package_path(
1852 &self,
1853 root: &ProjectRoot,
1854 target: &str,
1855 ) -> Result<PathBuf, PackageError> {
1856 let relative = target.strip_prefix("./").unwrap_or(target);
1859 if Path::new(relative)
1860 .components()
1861 .any(|component| component.as_os_str() == "node_modules")
1862 {
1863 return Err(PackageError::TargetUsesNodeModules {
1864 target: Arc::from(target),
1865 });
1866 }
1867 let resolved = root.resolve_from(&self.directory, relative)?;
1868 if !resolved.starts_with(&self.directory) {
1869 return Err(PackageError::TargetEscapesPackage {
1870 target: Arc::from(target),
1871 });
1872 }
1873 Ok(resolved)
1874 }
1875}
1876
1877fn package_invalid_field(field: impl Into<Arc<str>>, expected: &'static str) -> PackageError {
1878 PackageError::InvalidField {
1879 field: field.into(),
1880 expected,
1881 }
1882}
1883
1884fn package_optional_string(
1885 object: &JsonObject,
1886 key: &'static str,
1887) -> Result<Option<Arc<str>>, PackageError> {
1888 object
1889 .get(key)
1890 .map(|value| {
1891 value
1892 .as_str()
1893 .map(Arc::from)
1894 .ok_or_else(|| package_invalid_field(key, "a string"))
1895 })
1896 .transpose()
1897}
1898
1899fn validate_export_subpath(subpath: &str) -> Result<(), PackageError> {
1900 if subpath == "."
1901 || (subpath.starts_with("./")
1902 && subpath.len() > 2
1903 && !subpath.contains('\\')
1904 && !subpath
1905 .split('/')
1906 .any(|part| part == ".." || part.is_empty()))
1907 {
1908 Ok(())
1909 } else {
1910 Err(PackageError::InvalidSubpath {
1911 subpath: Arc::from(subpath),
1912 })
1913 }
1914}
1915
1916struct MapEntry<'a> {
1917 target: &'a JsonValue,
1918 capture: Option<Arc<str>>,
1919}
1920
1921fn select_map_entry<'a>(
1922 object: &'a JsonObject,
1923 request: &str,
1924) -> Result<Option<MapEntry<'a>>, PackageError> {
1925 if let Some(value) = object.get(request) {
1926 return Ok(Some(MapEntry {
1927 target: value,
1928 capture: None,
1929 }));
1930 }
1931 let mut selected: Option<(&JsonValue, Arc<str>, usize)> = None;
1932 for (pattern, value) in object.entries() {
1933 let stars = pattern.matches('*').count();
1934 if stars == 0 {
1935 continue;
1936 }
1937 if stars != 1 {
1938 return Err(PackageError::InvalidPattern {
1939 pattern: Arc::clone(pattern),
1940 });
1941 }
1942 let (prefix, suffix) =
1943 pattern
1944 .split_once('*')
1945 .ok_or_else(|| PackageError::InvalidPattern {
1946 pattern: Arc::clone(pattern),
1947 })?;
1948 let Some(remainder) = request.strip_prefix(prefix) else {
1949 continue;
1950 };
1951 let Some(capture) = remainder.strip_suffix(suffix) else {
1952 continue;
1953 };
1954 let specificity = prefix.len() + suffix.len();
1955 if selected
1956 .as_ref()
1957 .is_none_or(|(_, _, current)| specificity > *current)
1958 {
1959 selected = Some((value, Arc::from(capture), specificity));
1960 }
1961 }
1962 Ok(selected.map(|(value, capture, _)| MapEntry {
1963 target: value,
1964 capture: Some(capture),
1965 }))
1966}
1967
1968#[derive(Clone, Debug, Eq, PartialEq)]
1969enum TargetOutcome {
1970 Path(Arc<str>),
1971 External(Arc<str>),
1972 Blocked,
1973 NoMatch,
1974}
1975
1976fn resolve_package_target(
1977 value: &JsonValue,
1978 conditions: &ResolutionConditions,
1979 capture: Option<&str>,
1980) -> Result<TargetOutcome, PackageError> {
1981 match value {
1982 JsonValue::Null => Ok(TargetOutcome::Blocked),
1983 JsonValue::String(target) => {
1984 let target: Arc<str> = if let Some(capture) = capture {
1985 Arc::from(target.replace('*', capture))
1986 } else {
1987 Arc::clone(target)
1988 };
1989 if target.starts_with("./") {
1990 Ok(TargetOutcome::Path(target))
1991 } else if target.starts_with('/') || target.starts_with("../") {
1992 Err(PackageError::InvalidTarget { target })
1993 } else {
1994 Ok(TargetOutcome::External(target))
1995 }
1996 }
1997 JsonValue::Array(values) => {
1998 let mut blocked = false;
1999 for value in values.iter() {
2000 match resolve_package_target(value, conditions, capture)? {
2001 TargetOutcome::NoMatch => {}
2002 TargetOutcome::Blocked => blocked = true,
2003 outcome => return Ok(outcome),
2004 }
2005 }
2006 Ok(if blocked {
2007 TargetOutcome::Blocked
2008 } else {
2009 TargetOutcome::NoMatch
2010 })
2011 }
2012 JsonValue::Object(object) => {
2013 for (condition, target) in object.entries() {
2014 if conditions.contains(condition) {
2015 let outcome = resolve_package_target(target, conditions, capture)?;
2016 if outcome != TargetOutcome::NoMatch {
2017 return Ok(outcome);
2018 }
2019 }
2020 }
2021 Ok(TargetOutcome::NoMatch)
2022 }
2023 JsonValue::Bool(_) | JsonValue::Number(_) => Err(package_invalid_field(
2024 "exports/imports target",
2025 "a string, object, array, or null",
2026 )),
2027 }
2028}
2029
2030#[cfg(test)]
2031mod tests {
2032 use super::{
2033 ConfigError, JsonValue, JsoncErrorKind, LintTsConfig, ModuleResolutionError, PackageError,
2034 PackageJson, PackageMode, PackageTarget, ProjectConfig, ProjectRoot, ResolutionConditions,
2035 ResolutionFlavor, parse_bamts_toml, parse_jsonc, plan_relative_module,
2036 };
2037 use std::path::{Path, PathBuf};
2038
2039 fn root() -> ProjectRoot {
2040 ProjectRoot::new("/workspace/corpus").expect("absolute test root")
2041 }
2042
2043 #[test]
2044 fn bamts_toml_reads_only_lint_groups_and_rules() {
2045 let config = parse_bamts_toml(
2046 r#"
2047 title = "ignored"
2048 [lints.groups]
2049 escape-hatches = "deny"
2050 [unrelated]
2051 setting = "ignored"
2052 [lints.rules]
2053 BAMTS-W017 = "forbid" # exact rule
2054 "#,
2055 )
2056 .expect("valid bamts.toml lint tables");
2057 assert_eq!(config.groups().len(), 1);
2058 assert_eq!(config.groups()[0].name(), "escape-hatches");
2059 assert_eq!(config.rules().len(), 1);
2060 assert_eq!(config.rules()[0].name(), "BAMTS-W017");
2061 assert_eq!(config.rules()[0].source(), "bamts.toml:8");
2062 }
2063
2064 #[test]
2065 fn lint_tsconfig_ignores_typescript_strictness_options() {
2066 let config = LintTsConfig::parse(
2067 &root(),
2068 "/workspace/corpus/tsconfig.json",
2069 r#"{
2070 "compilerOptions": {
2071 "target": "ES2022",
2072 "module": "NodeNext",
2073 "moduleResolution": "NodeNext",
2074 "paths": {"@app/*": ["src/*"]},
2075 "strict": true,
2076 "useDefineForClassFields": false
2077 }
2078 }"#,
2079 )
2080 .expect("supported tsconfig view");
2081 assert_eq!(config.target(), Some("ES2022"));
2082 assert_eq!(config.module(), Some("NodeNext"));
2083 assert_eq!(config.module_resolution(), Some("NodeNext"));
2084 assert_eq!(config.paths()[0].pattern(), "@app/*");
2085 }
2086
2087 #[test]
2088 fn jsonc_accepts_tsconfig_comments_trailing_commas_and_surrogate_pairs() {
2089 let parsed = parse_jsonc(
2090 r#"{
2091 // TypeScript permits line comments.
2092 "compilerOptions": {
2093 "module": "NodeNext", /* and block comments */
2094 "types": ["node",],
2095 "icon": "\uD83D\uDE80",
2096 },
2097 }"#,
2098 )
2099 .expect("valid JSONC");
2100 let compiler = parsed
2101 .as_object()
2102 .and_then(|object| object.get("compilerOptions"))
2103 .and_then(JsonValue::as_object)
2104 .expect("compiler options object");
2105 assert_eq!(compiler.get("icon").and_then(JsonValue::as_str), Some("🚀"));
2106 assert_eq!(
2107 compiler
2108 .get("types")
2109 .and_then(JsonValue::as_array)
2110 .map(<[JsonValue]>::len),
2111 Some(1)
2112 );
2113 }
2114
2115 #[test]
2116 fn jsonc_rejects_duplicate_keys_and_unterminated_comments_with_offsets() {
2117 let duplicate = parse_jsonc(r#"{"x": 1, "x": 2}"#).expect_err("duplicate must fail");
2118 assert!(matches!(
2119 duplicate.kind(),
2120 JsoncErrorKind::DuplicateObjectKey { key } if key.as_ref() == "x"
2121 ));
2122 assert_eq!(duplicate.offset(), 9);
2123
2124 let comment = parse_jsonc("{/* never closed").expect_err("comment must terminate");
2125 assert_eq!(comment.offset(), 1);
2126 assert_eq!(comment.kind(), &JsoncErrorKind::UnterminatedBlockComment);
2127 }
2128
2129 #[test]
2130 fn jsonc_rejects_non_json_numbers_and_lone_surrogates() {
2131 assert_eq!(
2132 parse_jsonc("01").expect_err("leading zero").kind(),
2133 &JsoncErrorKind::InvalidNumber
2134 );
2135 assert_eq!(
2136 parse_jsonc(r#""\uD800""#)
2137 .expect_err("lone surrogate")
2138 .kind(),
2139 &JsoncErrorKind::LoneSurrogate
2140 );
2141 }
2142
2143 #[test]
2144 fn root_normalization_is_lexical_and_rejects_escape() {
2145 let project = root();
2146 assert_eq!(
2147 project
2148 .resolve("projects/ohash/src/../src/index.ts")
2149 .expect("confined path"),
2150 PathBuf::from("/workspace/corpus/projects/ohash/src/index.ts")
2151 );
2152 assert!(project.resolve("../secrets.ts").is_err());
2153 assert!(ProjectRoot::new("relative/root").is_err());
2154 }
2155
2156 #[test]
2157 fn project_config_parses_corpus_shaped_jsonc_into_immutable_options() {
2158 let config = ProjectConfig::parse(
2159 &root(),
2160 "/workspace/corpus/projects/hookable/tsconfig.json",
2161 r##"{
2162 "compilerOptions": {
2163 "target": "ESNext",
2164 "module": "NodeNext",
2165 "moduleResolution": "NodeNext",
2166 "strict": true,
2167 "resolveJsonModule": true,
2168 "baseUrl": ".",
2169 "paths": { "#src/*": ["src/*"] },
2170 },
2171 "include": ["src", "test",],
2172 }"##,
2173 )
2174 .expect("corpus-shaped config");
2175 assert_eq!(config.options().module(), Some("NodeNext"));
2176 assert!(config.options().strict());
2177 assert!(config.options().resolve_json_module());
2178 assert_eq!(
2179 config.options().base_url(),
2180 Path::new("/workspace/corpus/projects/hookable")
2181 );
2182 assert_eq!(
2183 config.options().paths()[0].targets()[0],
2184 PathBuf::from("/workspace/corpus/projects/hookable/src/*")
2185 );
2186 assert_eq!(config.include()[1].as_ref(), "test");
2187 }
2188
2189 #[test]
2190 fn project_config_rejects_wrong_types_and_every_root_escape() {
2191 let wrong = ProjectConfig::parse(
2192 &root(),
2193 "/workspace/corpus/tsconfig.json",
2194 r#"{"compilerOptions":{"strict":"yes"}}"#,
2195 )
2196 .expect_err("wrong type");
2197 assert!(matches!(wrong, ConfigError::InvalidField { .. }));
2198
2199 let escape = ProjectConfig::parse(
2200 &root(),
2201 "/workspace/corpus/tsconfig.json",
2202 r#"{"compilerOptions":{"outDir":"../outside"}}"#,
2203 )
2204 .expect_err("outDir escape");
2205 assert!(matches!(escape, ConfigError::Path(_)));
2206
2207 let include_escape = ProjectConfig::parse(
2208 &root(),
2209 "/workspace/corpus/tsconfig.json",
2210 r#"{"include":["../outside/**/*.ts"]}"#,
2211 )
2212 .expect_err("include escape");
2213 assert!(matches!(include_escape, ConfigError::Path(_)));
2214 }
2215
2216 #[test]
2217 fn relative_module_plan_covers_extension_substitution_and_index_without_io() {
2218 let project = root();
2219 let explicit = plan_relative_module(
2220 &project,
2221 "/workspace/corpus/cases/dot-prop.ts",
2222 "../projects/dot-prop/index.js",
2223 ResolutionFlavor::Runtime,
2224 false,
2225 )
2226 .expect("relative JS plan");
2227 assert_eq!(
2228 &explicit.candidates()[..4],
2229 &[
2230 PathBuf::from("/workspace/corpus/projects/dot-prop/index.ts"),
2231 PathBuf::from("/workspace/corpus/projects/dot-prop/index.tsx"),
2232 PathBuf::from("/workspace/corpus/projects/dot-prop/index.d.ts"),
2233 PathBuf::from("/workspace/corpus/projects/dot-prop/index.js"),
2234 ]
2235 );
2236
2237 let directory = plan_relative_module(
2238 &project,
2239 "/workspace/corpus/cases/ohash.ts",
2240 "../projects/ohash/src/crypto/node",
2241 ResolutionFlavor::Runtime,
2242 false,
2243 )
2244 .expect("extension and index plan");
2245 assert!(directory.candidates().contains(&PathBuf::from(
2246 "/workspace/corpus/projects/ohash/src/crypto/node/index.ts"
2247 )));
2248 assert_eq!(
2249 directory.select(|path| path.ends_with("index.ts")),
2250 Some(Path::new(
2251 "/workspace/corpus/projects/ohash/src/crypto/node/index.ts"
2252 ))
2253 );
2254 }
2255
2256 #[test]
2257 fn relative_module_plan_rejects_bare_url_unsupported_and_escaping_specifiers() {
2258 let project = root();
2259 let importer = "/workspace/corpus/cases/main.ts";
2260 assert!(matches!(
2261 plan_relative_module(
2262 &project,
2263 importer,
2264 "node:fs",
2265 ResolutionFlavor::Runtime,
2266 false
2267 ),
2268 Err(ModuleResolutionError::BareSpecifier { .. })
2269 ));
2270 assert!(matches!(
2271 plan_relative_module(
2272 &project,
2273 importer,
2274 "./x.ts?raw",
2275 ResolutionFlavor::Runtime,
2276 false
2277 ),
2278 Err(ModuleResolutionError::UrlLikeSpecifier { .. })
2279 ));
2280 assert!(
2281 plan_relative_module(
2282 &project,
2283 importer,
2284 "../../outside",
2285 ResolutionFlavor::Runtime,
2286 false
2287 )
2288 .is_err()
2289 );
2290 }
2291
2292 #[test]
2293 fn package_exports_cover_corpus_string_conditional_subpath_and_wildcard_shapes() {
2294 let project = root();
2295 let flat = PackageJson::parse(
2296 &project,
2297 "/workspace/corpus/projects/escape-string-regexp/package.json",
2298 r#"{"name":"escape-string-regexp","type":"module","exports":"./index.js"}"#,
2299 )
2300 .expect("flat corpus package");
2301 assert_eq!(
2302 flat.resolve_export(
2303 &project,
2304 ".",
2305 PackageMode::Import,
2306 &ResolutionConditions::for_mode(PackageMode::Import)
2307 )
2308 .expect("root export"),
2309 PathBuf::from("/workspace/corpus/projects/escape-string-regexp/index.js")
2310 );
2311
2312 let nested = PackageJson::parse(
2313 &project,
2314 "/workspace/corpus/projects/ohash/package.json",
2315 r#"{
2316 "name":"ohash",
2317 "exports": {
2318 ".":"./dist/index.mjs",
2319 "./crypto":{"node":"./dist/crypto/node/index.mjs","default":"./dist/crypto/js/index.mjs"},
2320 "./*":"./src/*.ts"
2321 }
2322 }"#,
2323 )
2324 .expect("nested corpus package");
2325 assert_eq!(
2326 nested
2327 .resolve_export(
2328 &project,
2329 "./crypto",
2330 PackageMode::Import,
2331 &ResolutionConditions::for_mode(PackageMode::Import)
2332 )
2333 .expect("node condition"),
2334 PathBuf::from("/workspace/corpus/projects/ohash/dist/crypto/node/index.mjs")
2335 );
2336 assert_eq!(
2337 nested
2338 .resolve_export(
2339 &project,
2340 "./serialize",
2341 PackageMode::Import,
2342 &ResolutionConditions::for_mode(PackageMode::Import)
2343 )
2344 .expect("wildcard"),
2345 PathBuf::from("/workspace/corpus/projects/ohash/src/serialize.ts")
2346 );
2347 }
2348
2349 #[test]
2350 fn package_conditions_preserve_declaration_precedence_and_types_branch() {
2351 let project = root();
2352 let package = PackageJson::parse(
2353 &project,
2354 "/workspace/corpus/projects/defu/package.json",
2355 r#"{
2356 "exports": {
2357 ".": {
2358 "types": "./dist/defu.d.mts",
2359 "import": {"default":"./dist/defu.mjs"},
2360 "require":"./lib/defu.cjs"
2361 }
2362 }
2363 }"#,
2364 )
2365 .expect("defu-shaped package");
2366 assert_eq!(
2367 package
2368 .resolve_export(
2369 &project,
2370 ".",
2371 PackageMode::Types,
2372 &ResolutionConditions::for_mode(PackageMode::Types)
2373 )
2374 .expect("types condition"),
2375 PathBuf::from("/workspace/corpus/projects/defu/dist/defu.d.mts")
2376 );
2377 }
2378
2379 #[test]
2380 fn package_imports_return_confined_or_explicit_external_targets() {
2381 let project = root();
2382 let package = PackageJson::parse(
2383 &project,
2384 "/workspace/corpus/projects/demo/package.json",
2385 r##"{
2386 "imports": {
2387 "#internal/*": "./src/*.ts",
2388 "#dependency": "dep/subpath"
2389 }
2390 }"##,
2391 )
2392 .expect("imports map");
2393 assert_eq!(
2394 package
2395 .resolve_import(
2396 &project,
2397 "#internal/value",
2398 &ResolutionConditions::for_mode(PackageMode::Import)
2399 )
2400 .expect("local import"),
2401 PackageTarget::Path(PathBuf::from(
2402 "/workspace/corpus/projects/demo/src/value.ts"
2403 ))
2404 );
2405 assert_eq!(
2406 package
2407 .resolve_import(
2408 &project,
2409 "#dependency",
2410 &ResolutionConditions::for_mode(PackageMode::Import)
2411 )
2412 .expect("external import"),
2413 PackageTarget::External("dep/subpath".into())
2414 );
2415 }
2416
2417 #[test]
2418 fn package_targets_cannot_escape_or_reenter_node_modules() {
2419 let project = root();
2420 let escape = PackageJson::parse(
2421 &project,
2422 "/workspace/corpus/projects/demo/package.json",
2423 r#"{"exports":"./../outside.js"}"#,
2424 )
2425 .expect("metadata parses");
2426 assert!(matches!(
2427 escape.resolve_export(
2428 &project,
2429 ".",
2430 PackageMode::Import,
2431 &ResolutionConditions::for_mode(PackageMode::Import)
2432 ),
2433 Err(PackageError::TargetEscapesPackage { .. })
2434 ));
2435
2436 let node_modules = PackageJson::parse(
2437 &project,
2438 "/workspace/corpus/projects/demo/package.json",
2439 r#"{"exports":"./node_modules/dep/index.js"}"#,
2440 )
2441 .expect("metadata parses");
2442 assert!(matches!(
2443 node_modules.resolve_export(
2444 &project,
2445 ".",
2446 PackageMode::Import,
2447 &ResolutionConditions::for_mode(PackageMode::Import)
2448 ),
2449 Err(PackageError::TargetUsesNodeModules { .. })
2450 ));
2451 }
2452
2453 #[test]
2454 fn legacy_package_entries_are_mode_specific_and_need_not_exist() {
2455 let project = root();
2456 let package = PackageJson::parse(
2457 &project,
2458 "/workspace/corpus/projects/tslib/package.json",
2459 r#"{
2460 "main":"./tslib.js",
2461 "module":"./tslib.es6.js",
2462 "typings":"./tslib.d.ts"
2463 }"#,
2464 )
2465 .expect("legacy corpus package");
2466 let conditions = ResolutionConditions::for_mode(PackageMode::Import);
2467 assert_eq!(
2468 package
2469 .resolve_export(&project, ".", PackageMode::Import, &conditions)
2470 .expect("module entry"),
2471 PathBuf::from("/workspace/corpus/projects/tslib/tslib.es6.js")
2472 );
2473 assert_eq!(
2474 package
2475 .resolve_export(&project, ".", PackageMode::Types, &conditions)
2476 .expect("types entry"),
2477 PathBuf::from("/workspace/corpus/projects/tslib/tslib.d.ts")
2478 );
2479
2480 let bare = PackageJson::parse(
2482 &project,
2483 "/workspace/corpus/projects/mitt/package.json",
2484 r#"{
2485 "main":"dist/mitt.js",
2486 "module":"dist/mitt.mjs",
2487 "typings":"index.d.ts"
2488 }"#,
2489 )
2490 .expect("bare legacy package");
2491 assert_eq!(
2492 bare.resolve_export(&project, ".", PackageMode::Require, &conditions)
2493 .expect("bare main entry"),
2494 PathBuf::from("/workspace/corpus/projects/mitt/dist/mitt.js")
2495 );
2496 assert_eq!(
2497 bare.resolve_export(&project, ".", PackageMode::Import, &conditions)
2498 .expect("bare module entry"),
2499 PathBuf::from("/workspace/corpus/projects/mitt/dist/mitt.mjs")
2500 );
2501 assert_eq!(
2502 bare.resolve_export(&project, ".", PackageMode::Types, &conditions)
2503 .expect("bare typings entry"),
2504 PathBuf::from("/workspace/corpus/projects/mitt/index.d.ts")
2505 );
2506 }
2507}