Skip to content

Commit

Permalink
fix(span/source-type): consider .cjs and .cts files as `ModuleKin…
Browse files Browse the repository at this point in the history
…d::Script` (#5239)

- fix: `SourceType::from_path` considers `.cjs` and `.cts` as modules, not scripts
- docs: improve rusdoc for `SourceType::from_path`
- test: add unit tests for `SourceType::from_path`
  • Loading branch information
DonIsaac committed Aug 27, 2024
1 parent 1af7f04 commit a6bb3b1
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 14 deletions.
4 changes: 3 additions & 1 deletion crates/oxc_span/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod span;

pub use crate::{
atom::{Atom, CompactStr, MAX_INLINE_LEN as ATOM_MAX_INLINE_LEN},
source_type::{Language, LanguageVariant, ModuleKind, SourceType, VALID_EXTENSIONS},
source_type::{
Language, LanguageVariant, ModuleKind, SourceType, UnknownExtension, VALID_EXTENSIONS,
},
span::{GetSpan, GetSpanMut, Span, SPAN},
};
183 changes: 170 additions & 13 deletions crates/oxc_span/src/source_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ impl SourceType {
self.language == Language::JavaScript
}

/// Returns `true` if this is a TypeScript file or TypeScript definition file.
///
/// I.e., `true` for `.ts`, `.cts`, `.mts`, `.tsx`, and `.d.ts` files.
pub fn is_typescript(self) -> bool {
matches!(self.language, Language::TypeScript | Language::TypeScriptDefinition)
}
Expand Down Expand Up @@ -114,11 +117,44 @@ impl SourceType {
self
}

/// Converts file path to `SourceType`
/// returns `SourceTypeError::UnknownExtension` if:
/// * there is no file name
/// * the file extension is not one of "js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx"
/// Converts a file [`Path`] to [`SourceType`].
///
/// ## Examples
/// ```
/// # use oxc_span::SourceType;
///
/// // supports .ts, .mts, .cts, .tsx, .d.ts, etc.
/// let ts = SourceType::from_path("foo.ts").unwrap();
/// assert!(ts.is_typescript());
/// assert!(!ts.is_typescript_definition());
///
/// // supports .js, .mjs, .cjs, .jsx
/// let jsx = SourceType::from_path("foo.jsx").unwrap();
/// assert!(jsx.is_javascript());
/// assert!(jsx.is_jsx());
/// ```
///
/// ## Behavior
/// ### JSX
/// All JavaScript-like files are treated as JSX, since some tools (like
/// babel) also do not make a distinction between `.js` and `.jsx`. However,
/// for TypeScript files, only `.tsx` files are treated as JSX.
///
/// ### Modules vs. Scripts.
/// Oxc has partial support for Node's
/// [CommonJS](https://nodejs.org/api/modules.html#enabling) detection
/// strategy. Any file with a `.c[tj]s` extension is treated as a [`script`].
/// All other files are treated as [`modules`].
///
/// # Errors
/// Returns [`UnknownExtension`] if:
/// * there is no file name
/// * the file extension is not one of "js", "mjs", "cjs", "jsx", "ts",
/// "mts", "cts", "tsx". See [`VALID_EXTENSIONS`] for the list of valid
/// extensions.
///
/// [`script`]: ModuleKind::Script
/// [`modules`]: ModuleKind::Module
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, UnknownExtension> {
let file_name = path
.as_ref()
Expand All @@ -134,18 +170,29 @@ impl SourceType {
.ok_or_else(|| {
let path = path.as_ref().to_string_lossy();
UnknownExtension(
format!("Please provide a valid file extension for {path}: .js, .mjs, .jsx or .cjs for JavaScript, or .ts, .mts, .cts or .tsx for TypeScript"),
format!("Please provide a valid file extension for {path}: .js, .mjs, .jsx or .cjs for JavaScript, or .ts, .d.ts, .mts, .cts or .tsx for TypeScript"),
)
})?;

let language = match extension {
"js" | "mjs" | "cjs" | "jsx" => Language::JavaScript,
"ts" if file_name.ends_with(".d.ts") => Language::TypeScriptDefinition,
"mts" if file_name.ends_with(".d.mts") => Language::TypeScriptDefinition,
"cts" if file_name.ends_with(".d.cts") => Language::TypeScriptDefinition,
let (language, module_kind) = match extension {
"js" | "mjs" | "jsx" => (Language::JavaScript, ModuleKind::Module),
"cjs" => (Language::JavaScript, ModuleKind::Script),
"ts" if file_name.ends_with(".d.ts") => {
(Language::TypeScriptDefinition, ModuleKind::Module)
}
"mts" if file_name.ends_with(".d.mts") => {
(Language::TypeScriptDefinition, ModuleKind::Module)
}
"cts" if file_name.ends_with(".d.cts") => {
(Language::TypeScriptDefinition, ModuleKind::Script)
}
"ts" | "mts" | "tsx" => (Language::TypeScript, ModuleKind::Module),
"cts" => (Language::TypeScript, ModuleKind::Script),
_ => {
debug_assert!(matches!(extension, "ts" | "mts" | "cts" | "tsx"));
Language::TypeScript
#[cfg(debug_assertions)]
unreachable!();
#[cfg(not(debug_assertions))]
return Err(UnknownExtension(format!("Unknown extension: {}", extension)));
}
};

Expand All @@ -154,6 +201,116 @@ impl SourceType {
_ => LanguageVariant::Standard,
};

Ok(Self { language, module_kind: ModuleKind::Module, variant, always_strict: false })
Ok(Self { language, module_kind, variant, always_strict: false })
}
}

#[cfg(test)]
mod tests {
use super::SourceType;

#[test]
#[allow(clippy::similar_names)]
fn test_ts() {
let ts = SourceType::from_path("foo.ts")
.expect("foo.ts should be a valid TypeScript file path.");
let mts = SourceType::from_path("foo.mts")
.expect("foo.mts should be a valid TypeScript file path.");
let cts = SourceType::from_path("foo.cts")
.expect("foo.cts should be a valid TypeScript file path.");
let tsx = SourceType::from_path("foo.tsx")
.expect("foo.tsx should be a valid TypeScript file path.");

for ty in &[ts, mts, cts, tsx] {
assert!(ty.is_typescript());
assert!(!ty.is_typescript_definition());
assert!(!ty.is_javascript());
}

assert!(ts.is_module());
assert!(mts.is_module());
assert!(!cts.is_module());
assert!(tsx.is_module());

assert!(!ts.is_script());
assert!(!mts.is_script());
assert!(cts.is_script());
assert!(!tsx.is_script());

assert!(ts.is_strict());
assert!(mts.is_strict());
assert!(!cts.is_strict());
assert!(tsx.is_strict());

assert!(!ts.is_jsx());
assert!(!mts.is_jsx());
assert!(!cts.is_jsx());
assert!(tsx.is_jsx());
}

#[test]
#[allow(clippy::similar_names)]
fn test_d_ts() {
let dts = SourceType::from_path("foo.d.ts")
.expect("foo.d.ts should be a valid TypeScript definition file path.");
let dmts = SourceType::from_path("foo.d.mts")
.expect("foo.d.mts should be a valid TypeScript definition file path.");
let dcts = SourceType::from_path("foo.d.cts")
.expect("foo.d.cts should be a valid TypeScript definition file path.");

for ty in &[dts, dmts, dcts] {
assert!(ty.is_typescript());
assert!(ty.is_typescript_definition());
assert!(!ty.is_javascript());
}

assert!(dts.is_module());
assert!(dmts.is_module());
assert!(!dcts.is_module());

assert!(!dts.is_script());
assert!(!dmts.is_script());
assert!(dcts.is_script());

assert!(dts.is_strict());
assert!(dmts.is_strict());
assert!(!dcts.is_strict());

assert!(!dts.is_jsx());
assert!(!dmts.is_jsx());
assert!(!dcts.is_jsx());
}

#[test]
#[allow(clippy::similar_names)]
fn test_js() {
let js = SourceType::from_path("foo.js")
.expect("foo.js should be a valid JavaScript file path.");
let mjs = SourceType::from_path("foo.mjs")
.expect("foo.mjs should be a valid JavaScript file path.");
let cjs = SourceType::from_path("foo.cjs")
.expect("foo.cjs should be a valid JavaScript file path.");
let jsx = SourceType::from_path("foo.jsx")
.expect("foo.jsx should be a valid JavaScript file path.");

for ty in &[js, mjs, cjs, jsx] {
assert!(ty.is_javascript(), "{ty:?}");
assert!(!ty.is_typescript(), "{ty:?}");
}

assert!(js.is_module());
assert!(mjs.is_module());
assert!(cjs.is_script());
assert!(jsx.is_module());

assert!(js.is_strict());
assert!(mjs.is_strict());
assert!(!cjs.is_strict());
assert!(jsx.is_strict());

assert!(js.is_jsx());
assert!(mjs.is_jsx());
assert!(cjs.is_jsx());
assert!(jsx.is_jsx());
}
}
3 changes: 3 additions & 0 deletions crates/oxc_span/src/source_type/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct SourceType {
pub(super) variant: LanguageVariant,

/// Mark strict mode as always strict
///
/// See <https://github.com/tc39/test262/blob/main/INTERPRETING.md#strict-mode>
pub(super) always_strict: bool,
}
Expand All @@ -43,7 +44,9 @@ pub enum Language {
#[cfg_attr(feature = "serialize", derive(Serialize, Tsify))]
#[serde(rename_all = "camelCase")]
pub enum ModuleKind {
/// Regular JS script or CommonJS file
Script = 0,
/// ES6 Module
Module = 1,
}

Expand Down

0 comments on commit a6bb3b1

Please sign in to comment.