Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 44 additions & 32 deletions crates/oxc_linter/src/loader/partial_loader/vue.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use memchr::memmem::Finder;

use oxc_span::SourceType;

use super::{JavaScriptSource, SCRIPT_END, SCRIPT_START, find_script_closing_angle};
Expand Down Expand Up @@ -48,8 +49,21 @@ impl<'a> VuePartialLoader<'a> {

// get ts and jsx attribute
let content = &self.source_text[*pointer..*pointer + offset];
let is_ts = content.contains("ts");
let is_jsx = content.contains("tsx") || content.contains("jsx");

// parse `lang`
let lang = content.split_once("lang").map_or(Some("mjs"), |(_, s)| {
const QUOTES: [char; 2] = ['"', '\''];
s.trim_start()
.trim_start_matches('=')
.trim_start()
.trim_start_matches(QUOTES)
.split_once(QUOTES)
.map(|(s, _)| s)
})?;
let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
if !lang.contains('x') {
source_type = source_type.with_standard(true);
}

*pointer += offset + 1;
let js_start = *pointer;
Expand All @@ -61,7 +75,6 @@ impl<'a> VuePartialLoader<'a> {
*pointer += offset + SCRIPT_END.len();

let source_text = &self.source_text[js_start..js_end];
let source_type = SourceType::mjs().with_typescript(is_ts).with_jsx(is_jsx);
// NOTE: loader checked that source_text.len() is less than u32::MAX
#[expect(clippy::cast_possible_truncation)]
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
Expand All @@ -70,6 +83,8 @@ impl<'a> VuePartialLoader<'a> {

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

use super::{JavaScriptSource, VuePartialLoader};

fn parse_vue(source_text: &str) -> JavaScriptSource<'_> {
Expand Down Expand Up @@ -99,20 +114,7 @@ mod test {
"#;

let result = parse_vue(source_text);
assert!(result.source_type.is_typescript());
assert_eq!(result.source_text.trim(), "1/1");
}

#[test]
fn test_build_vue_with_ts_flag_2() {
let source_text = r"
<script lang=ts setup>
1/1
</script>
";

let result = parse_vue(source_text);
assert!(result.source_type.is_typescript());
assert_eq!(result.source_type, SourceType::ts());
assert_eq!(result.source_text.trim(), "1/1");
}

Expand All @@ -125,21 +127,7 @@ mod test {
";

let result = parse_vue(source_text);
assert!(result.source_type.is_typescript());
assert_eq!(result.source_text.trim(), "1/1");
}

#[test]
fn test_build_vue_with_tsx_flag() {
let source_text = r"
<script lang=tsx setup>
1/1
</script>
";

let result = parse_vue(source_text);
assert!(result.source_type.is_jsx());
assert!(result.source_type.is_typescript());
assert_eq!(result.source_type, SourceType::ts());
assert_eq!(result.source_text.trim(), "1/1");
}

Expand Down Expand Up @@ -252,4 +240,28 @@ mod test {
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].source_text, "a");
}

#[test]
fn lang() {
let cases = [
("<script>debugger</script>", Some(SourceType::mjs())),
("<script lang = 'tsx' >debugger</script>", Some(SourceType::tsx())),
(r#"<script lang = "cjs" >debugger</script>"#, Some(SourceType::cjs())),
("<script lang = 'xxx'>debugger</script>", None),
(r#"<script lang = "xxx">debugger</script>"#, None),
("<script lang='xxx'>debugger</script>", None),
(r#"<script lang="xxx">debugger</script>"#, None),
("<script lang=tsx>debugger</script>", None), // this is valid but too compliated to parse
];

for (source_text, source_type) in cases {
let sources = VuePartialLoader::new(source_text).parse();
if let Some(expected) = source_type {
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].source_type, expected);
} else {
assert_eq!(sources.len(), 0);
}
}
}
}
48 changes: 28 additions & 20 deletions crates/oxc_span/src/source_type/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,36 +462,44 @@ impl SourceType {
)
})?;

let mut source_type = Self::from_extension(extension)?;

#[expect(clippy::case_sensitive_file_extension_comparisons)]
if match extension {
"ts" if file_name[..file_name.len() - 3].split('.').rev().take(2).any(|c| c == "d") => {
true
}
"mts" | "cts" if file_name[..file_name.len() - 4].ends_with(".d") => true,
_ => false,
} {
source_type.language = Language::TypeScriptDefinition;
}

Ok(source_type)
}

/// Converts a file extension to [`SourceType`].
///
/// # Errors
///
/// Returns [`UnknownExtension`] if:
/// * the file extension is not one of "js", "mjs", "cjs", "jsx", "ts",
/// "mts", "cts", "tsx". See [`VALID_EXTENSIONS`] for the list of valid
/// extensions.
pub fn from_extension(extension: &str) -> Result<Self, UnknownExtension> {
let module_kind = match extension {
"js" | "tsx" | "ts" | "jsx" | "mts" | "mjs" => ModuleKind::Module,
"cjs" | "cts" => ModuleKind::Script,
_ => unreachable!(),
_ => return Err(UnknownExtension::new("Unknown extension.")),
};

let language = match extension {
// https://www.typescriptlang.org/tsconfig/#allowArbitraryExtensions
// `{file basename}.d.{extension}.ts`
// https://github.com/microsoft/TypeScript/issues/50133
"ts" => {
if file_name[..file_name.len() - 3].split('.').rev().take(2).any(|c| c == "d") {
Language::TypeScriptDefinition
} else {
Language::TypeScript
}
}
"js" | "cjs" | "mjs" | "jsx" => Language::JavaScript,
"tsx" => Language::TypeScript,
#[expect(clippy::case_sensitive_file_extension_comparisons)]
"mts" | "cts" => {
if file_name[..file_name.len() - 4].ends_with(".d") {
Language::TypeScriptDefinition
} else {
Language::TypeScript
}
}
_ => {
unreachable!();
}
"ts" | "tsx" | "mts" | "cts" => Language::TypeScript,
_ => return Err(UnknownExtension::new("Unknown extension.")),
};

let variant = match extension {
Expand Down
Loading