diff --git a/crates/oxc_linter/src/context/host.rs b/crates/oxc_linter/src/context/host.rs index f204aa9063c16..49e9fc05932a2 100644 --- a/crates/oxc_linter/src/context/host.rs +++ b/crates/oxc_linter/src/context/host.rs @@ -1,6 +1,7 @@ use std::{ borrow::Cow, cell::{Cell, RefCell}, + ffi::OsStr, path::Path, rc::Rc, sync::Arc, @@ -141,6 +142,8 @@ pub struct ContextHost<'a> { pub(super) fix: FixKind, /// Path to the file being linted. pub(super) file_path: Box, + /// Extension of the file being linted. + file_extension: Option>, /// Global linter configuration, such as globals to include and the target /// environments, and other settings. pub(super) config: Arc, @@ -171,6 +174,7 @@ impl<'a> ContextHost<'a> { ); let file_path = file_path.as_ref().to_path_buf().into_boxed_path(); + let file_extension = file_path.extension().map(|ext| ext.to_owned().into_boxed_os_str()); Self { sub_hosts, @@ -178,6 +182,7 @@ impl<'a> ContextHost<'a> { diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)), fix: options.fix, file_path, + file_extension, config, frameworks: options.framework_hints, } @@ -231,6 +236,12 @@ impl<'a> ContextHost<'a> { &self.file_path } + /// Extension of the file currently being linted, without the leading dot. + #[inline] + pub fn file_extension(&self) -> Option<&OsStr> { + self.file_extension.as_deref() + } + /// The source type of the file being linted, e.g. JavaScript, TypeScript, /// CJS, ESM, etc. #[inline] diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index 548182b175403..b14e112eb1e81 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -1,6 +1,6 @@ #![expect(rustdoc::private_intra_doc_links)] // useful for intellisense -use std::{ops::Deref, path::Path, rc::Rc}; +use std::{ffi::OsStr, ops::Deref, path::Path, rc::Rc}; use javascript_globals::GLOBALS; @@ -141,6 +141,12 @@ impl<'a> LintContext<'a> { &self.parent.file_path } + /// Extension of the file currently being linted, without the leading dot. + #[inline] + pub fn file_extension(&self) -> Option<&OsStr> { + self.parent.file_extension() + } + /// Plugin settings #[inline] pub fn settings(&self) -> &OxlintSettings { diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs b/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs index 80dca2aa3d9ed..8d3adc64f5687 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_labels.rs @@ -65,7 +65,7 @@ impl Rule for NoUnusedLabels { } fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { - ctx.file_path().extension().is_some_and(|ext| ext != "svelte") + ctx.file_extension().is_some_and(|ext| ext != "svelte") } } diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs index 5d62f10a2b67f..47ddd4765bf86 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs @@ -223,8 +223,7 @@ impl Rule for NoUnusedVars { // we can't detect !ctx.source_type().is_typescript_definition() && !ctx - .file_path() - .extension() + .file_extension() .is_some_and(|ext| ext == "vue" || ext == "svelte" || ext == "astro") } } diff --git a/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs b/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs index 5397d97f1b51c..5a413e18d9088 100644 --- a/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs +++ b/crates/oxc_linter/src/rules/jest/no_large_snapshots.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, path::Path}; +use std::ops::Deref; use lazy_regex::Regex; use oxc_ast::{ @@ -170,9 +170,7 @@ impl Rule for NoLargeSnapshots { } fn run_once(&self, ctx: &LintContext) { - let is_snap = ctx.file_path().to_str().is_some_and(|p| { - Path::new(p).extension().is_some_and(|ext| ext.eq_ignore_ascii_case("snap")) - }); + let is_snap = ctx.file_extension().is_some_and(|ext| ext.eq_ignore_ascii_case("snap")); if is_snap { for node in ctx.nodes().iter() { diff --git a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs index b9ab2e1965fad..48edb8d713f54 100644 --- a/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs +++ b/crates/oxc_linter/src/rules/react/jsx_filename_extension.rs @@ -157,7 +157,7 @@ impl Rule for JsxFilenameExtension { } fn run_once(&self, ctx: &LintContext) { - let file_extension = ctx.file_path().extension().and_then(OsStr::to_str).unwrap_or(""); + let file_extension = ctx.file_extension().and_then(OsStr::to_str).unwrap_or(""); let has_ext_allowed = self.extensions.contains(&CompactStr::new(file_extension)); if !has_ext_allowed { diff --git a/crates/oxc_linter/src/rules/react/only_export_components.rs b/crates/oxc_linter/src/rules/react/only_export_components.rs index 9c67797ca4b95..ba337d31e6c2c 100644 --- a/crates/oxc_linter/src/rules/react/only_export_components.rs +++ b/crates/oxc_linter/src/rules/react/only_export_components.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use oxc_ast::{AstKind, ast::*}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; @@ -262,7 +260,7 @@ impl Rule for OnlyExportComponents { }; let should_scan = { - let ext = Path::new(filename).extension().and_then(|e| e.to_str()); + let ext = ctx.file_extension(); matches!(ext, Some(e) if e.eq_ignore_ascii_case("tsx") || e.eq_ignore_ascii_case("jsx")) || (self.check_js && matches!(ext, Some(e) if e.eq_ignore_ascii_case("js"))) }; diff --git a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs index 4ab12e9f20276..36c213fcf2ed2 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -173,7 +173,7 @@ impl Rule for RulesOfHooks { // disable this rule in vue/nuxt and svelte(kit) files // react hook can be build in only `.ts` files, // but `useX` functions are popular and can be false positive in other frameworks - !ctx.file_path().extension().is_some_and(|ext| ext == "vue" || ext == "svelte") + !ctx.file_extension().is_some_and(|ext| ext == "vue" || ext == "svelte") } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { diff --git a/crates/oxc_linter/src/rules/unicorn/no_empty_file.rs b/crates/oxc_linter/src/rules/unicorn/no_empty_file.rs index 6a0a9d5be23b4..50cc701df65ca 100644 --- a/crates/oxc_linter/src/rules/unicorn/no_empty_file.rs +++ b/crates/oxc_linter/src/rules/unicorn/no_empty_file.rs @@ -62,9 +62,8 @@ impl Rule for NoEmptyFile { } fn should_run(&self, ctx: &ContextHost) -> bool { - ctx.file_path().extension().is_some_and(|ext| { - !LINT_PARTIAL_LOADER_EXTENSIONS.contains(&ext.to_string_lossy().as_ref()) - }) + ctx.file_extension() + .is_some_and(|ext| !LINT_PARTIAL_LOADER_EXTENSIONS.iter().any(|e| *e == ext)) } } diff --git a/crates/oxc_linter/src/rules/vue/max_props.rs b/crates/oxc_linter/src/rules/vue/max_props.rs index 287d7781ec518..7a996f144f5ce 100644 --- a/crates/oxc_linter/src/rules/vue/max_props.rs +++ b/crates/oxc_linter/src/rules/vue/max_props.rs @@ -105,7 +105,7 @@ impl Rule for MaxProps { } fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { - ctx.file_path().extension().is_some_and(|ext| ext == "vue") + ctx.file_extension().is_some_and(|ext| ext == "vue") } } diff --git a/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs b/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs index bb088630e2218..0ca89fc70cc31 100644 --- a/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs +++ b/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs @@ -144,7 +144,7 @@ impl Rule for NoMultipleSlotArgs { } fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { - ctx.file_path().extension().is_some_and(|ext| ext == "vue") + ctx.file_extension().is_some_and(|ext| ext == "vue") && ctx.frameworks_options() != FrameworkOptions::VueSetup } } diff --git a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs index 104723c2e41f0..6ca7129196b8c 100644 --- a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs +++ b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs @@ -73,7 +73,7 @@ declare_oxc_lint!( impl Rule for NoRequiredPropWithDefault { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - let is_vue = ctx.file_path().extension().is_some_and(|ext| ext == "vue"); + let is_vue = ctx.file_extension().is_some_and(|ext| ext == "vue"); if is_vue { self.run_on_vue(node, ctx); } else { diff --git a/crates/oxc_linter/src/rules/vue/require_default_export.rs b/crates/oxc_linter/src/rules/vue/require_default_export.rs index 5bc10b35d716c..fd358fa422a6a 100644 --- a/crates/oxc_linter/src/rules/vue/require_default_export.rs +++ b/crates/oxc_linter/src/rules/vue/require_default_export.rs @@ -93,7 +93,7 @@ impl Rule for RequireDefaultExport { fn should_run(&self, ctx: &ContextHost) -> bool { // only on vue files - if ctx.file_path().extension().is_none_or(|ext| ext != "vue") { + if ctx.file_extension().is_none_or(|ext| ext != "vue") { return false; }