Skip to content
Closed
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
6 changes: 5 additions & 1 deletion crates/oxc_linter/src/context/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
config::LintConfig,
disable_directives::{DisableDirectives, DisableDirectivesBuilder, RuleCommentType},
fixer::{Fix, FixKind, Message, PossibleFixes},
frameworks,
frameworks::{self, FrameworkOptions},
module_record::ModuleRecord,
options::LintOptions,
rules::RuleEnum,
Expand Down Expand Up @@ -63,6 +63,8 @@ pub struct ContextHost<'a> {
pub(super) config: Arc<LintConfig>,
/// Front-end frameworks that might be in use in the target file.
pub(super) frameworks: FrameworkFlags,
// Specific framework options, for example, whether the context is inside `<script setup>` in Vue files.
pub(super) frameworks_options: FrameworkOptions,
}

impl<'a> ContextHost<'a> {
Expand All @@ -74,6 +76,7 @@ impl<'a> ContextHost<'a> {
module_record: Arc<ModuleRecord>,
options: LintOptions,
config: Arc<LintConfig>,
frameworks_options: FrameworkOptions,
) -> Self {
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 512;

Expand All @@ -98,6 +101,7 @@ impl<'a> ContextHost<'a> {
file_path,
config,
frameworks: options.framework_hints,
frameworks_options,
}
.sniff_for_frameworks()
}
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_linter/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
config::GlobalValue,
disable_directives::DisableDirectives,
fixer::{Fix, FixKind, Message, PossibleFixes, RuleFix, RuleFixer},
frameworks::FrameworkOptions,
};

mod host;
Expand Down Expand Up @@ -114,6 +115,10 @@ impl<'a> LintContext<'a> {
self.parent.module_record()
}

pub fn frameworks_options(&self) -> &FrameworkOptions {
&self.parent.frameworks_options
}

/// Get the control flow graph for the current program.
#[inline]
pub fn cfg(&self) -> &ControlFlowGraph {
Expand Down
6 changes: 6 additions & 0 deletions crates/oxc_linter/src/frameworks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,9 @@ pub fn has_vitest_imports(module_record: &ModuleRecord) -> bool {
pub fn has_jest_imports(module_record: &ModuleRecord) -> bool {
module_record.import_entries.iter().any(|entry| entry.module_request.name() == "@jest/globals")
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum FrameworkOptions {
None, // default
VueSetup, // context is inside `<script setup>`
}
13 changes: 10 additions & 3 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub use crate::{
},
external_plugin_store::{ExternalPluginStore, ExternalRuleId},
fixer::FixKind,
frameworks::FrameworkFlags,
frameworks::{FrameworkFlags, FrameworkOptions},
loader::LINTABLE_EXTENSIONS,
module_record::ModuleRecord,
options::LintOptions,
Expand Down Expand Up @@ -127,12 +127,19 @@ impl Linter {
path: &Path,
semantic: Rc<Semantic<'a>>,
module_record: Arc<ModuleRecord>,
framework_options: FrameworkOptions,
allocator: &Allocator,
) -> Vec<Message<'a>> {
let ResolvedLinterState { rules, config, external_rules } = self.config.resolve(path);

let ctx_host =
Rc::new(ContextHost::new(path, semantic, module_record, self.options, config));
let ctx_host = Rc::new(ContextHost::new(
path,
semantic,
module_record,
self.options,
config,
framework_options,
));

let rules = rules
.iter()
Expand Down
5 changes: 3 additions & 2 deletions crates/oxc_linter/src/loader/partial_loader/astro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use memchr::memmem::Finder;

use oxc_span::{SourceType, Span};

use crate::loader::JavaScriptSource;
use crate::{frameworks::FrameworkOptions, loader::JavaScriptSource};

use super::{SCRIPT_END, SCRIPT_START};

Expand Down Expand Up @@ -47,7 +47,7 @@ impl<'a> AstroPartialLoader<'a> {
// move start to the end of the ASTRO_SPLIT
let start = start + ASTRO_SPLIT.len() as u32;
let js_code = Span::new(start, end).source_text(self.source_text);
Some(JavaScriptSource::partial(js_code, SourceType::ts(), start))
Some(JavaScriptSource::partial(js_code, SourceType::ts(), FrameworkOptions::None, start))
}

/// In .astro files, you can add client-side JavaScript by adding one (or more) `<script>` tags.
Expand Down Expand Up @@ -94,6 +94,7 @@ impl<'a> AstroPartialLoader<'a> {
results.push(JavaScriptSource::partial(
&self.source_text[js_start..js_end],
SourceType::ts(),
FrameworkOptions::None,
js_start as u32,
));
}
Expand Down
9 changes: 7 additions & 2 deletions crates/oxc_linter/src/loader/partial_loader/svelte.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use memchr::memmem::Finder;

use oxc_span::SourceType;

use crate::loader::JavaScriptSource;
use crate::{frameworks::FrameworkOptions, loader::JavaScriptSource};

use super::{SCRIPT_END, SCRIPT_START, find_script_closing_angle};

Expand Down Expand Up @@ -62,7 +62,12 @@ impl<'a> SveltePartialLoader<'a> {

// 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))
Some(JavaScriptSource::partial(
source_text,
source_type,
FrameworkOptions::None,
js_start as u32,
))
}
}

Expand Down
15 changes: 14 additions & 1 deletion crates/oxc_linter/src/loader/partial_loader/vue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use memchr::memmem::Finder;

use oxc_span::SourceType;

use crate::frameworks::FrameworkOptions;

use super::{JavaScriptSource, SCRIPT_END, SCRIPT_START, find_script_closing_angle};

pub struct VuePartialLoader<'a> {
Expand Down Expand Up @@ -52,6 +54,7 @@ impl<'a> VuePartialLoader<'a> {

// parse `lang`
let lang = Self::extract_lang_attribute(content);
let is_setup = content.contains("setup"); // check if "setup" is present, does not check if its inside an attribute

let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
if !lang.contains('x') {
Expand All @@ -70,7 +73,12 @@ impl<'a> VuePartialLoader<'a> {
let source_text = &self.source_text[js_start..js_end];
// 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))
Some(JavaScriptSource::partial(
source_text,
source_type,
if is_setup { FrameworkOptions::VueSetup } else { FrameworkOptions::None },
js_start as u32,
))
}

fn extract_lang_attribute(content: &str) -> &str {
Expand Down Expand Up @@ -115,6 +123,8 @@ impl<'a> VuePartialLoader<'a> {
mod test {
use oxc_span::SourceType;

use crate::FrameworkOptions;

use super::{JavaScriptSource, VuePartialLoader};

fn parse_vue(source_text: &str) -> JavaScriptSource<'_> {
Expand All @@ -132,6 +142,7 @@ mod test {
"#;

let result = parse_vue(source_text);
assert_eq!(result.framework_options, FrameworkOptions::None);
assert_eq!(result.source_text, r#" console.log("hi") "#);
}

Expand Down Expand Up @@ -229,7 +240,9 @@ mod test {
let sources = VuePartialLoader::new(source_text).parse();
assert_eq!(sources.len(), 2);
assert_eq!(sources[0].source_text, "a");
assert_eq!(sources[0].framework_options, FrameworkOptions::None);
assert_eq!(sources[1].source_text, "b");
assert_eq!(sources[1].framework_options, FrameworkOptions::VueSetup);
}

#[test]
Expand Down
22 changes: 19 additions & 3 deletions crates/oxc_linter/src/loader/source.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use oxc_span::SourceType;

use crate::frameworks::FrameworkOptions;

#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct JavaScriptSource<'a> {
Expand All @@ -10,15 +12,29 @@ pub struct JavaScriptSource<'a> {
pub start: u32,
#[expect(dead_code)]
is_partial: bool,

// some partial sources can have special options defined, like Vue's `<script setup>`.
pub framework_options: FrameworkOptions,
}

impl<'a> JavaScriptSource<'a> {
pub fn new(source_text: &'a str, source_type: SourceType) -> Self {
Self { source_text, source_type, start: 0, is_partial: false }
Self {
source_text,
source_type,
start: 0,
is_partial: false,
framework_options: FrameworkOptions::None,
}
}

pub fn partial(source_text: &'a str, source_type: SourceType, start: u32) -> Self {
Self { source_text, source_type, start, is_partial: true }
pub fn partial(
source_text: &'a str,
source_type: SourceType,
framework_options: FrameworkOptions,
start: u32,
) -> Self {
Self { source_text, source_type, start, is_partial: true, framework_options }
}

pub fn as_str(&self) -> &'a str {
Expand Down
9 changes: 7 additions & 2 deletions crates/oxc_linter/src/service/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use oxc_span::{CompactStr, SourceType, VALID_EXTENSIONS};
use crate::{
Fixer, Linter, Message,
fixer::PossibleFixes,
frameworks::FrameworkOptions,
loader::{JavaScriptSource, LINT_PARTIAL_LOADER_EXTENSIONS, PartialLoader},
module_record::ModuleRecord,
utils::read_to_arena_str,
Expand Down Expand Up @@ -524,6 +525,7 @@ impl Runtime {
path,
Rc::new(section.semantic.unwrap()),
Arc::clone(&module_record),
section.source.framework_options,
allocator_guard,
),
Err(errors) => errors
Expand Down Expand Up @@ -640,6 +642,7 @@ impl Runtime {
Path::new(&module.path),
Rc::new(section.semantic.unwrap()),
Arc::clone(&module_record),
section.source.framework_options,
allocator_guard,
),
};
Expand Down Expand Up @@ -763,6 +766,7 @@ impl Runtime {
Path::new(&module.path),
Rc::new(section.semantic.unwrap()),
Arc::clone(&module_record),
section.source.framework_options,
allocator_guard,
),
Err(errors) => errors
Expand Down Expand Up @@ -893,8 +897,9 @@ impl Runtime {
allocator: &'a Allocator,
mut out_sections: Option<&mut SectionContents<'a>>,
) -> SmallVec<[Result<ResolvedModuleRecord, Vec<OxcDiagnostic>>; 1]> {
let section_sources = PartialLoader::parse(ext, source_text)
.unwrap_or_else(|| vec![JavaScriptSource::partial(source_text, source_type, 0)]);
let section_sources = PartialLoader::parse(ext, source_text).unwrap_or_else(|| {
vec![JavaScriptSource::partial(source_text, source_type, FrameworkOptions::None, 0)]
});

let mut section_module_records = SmallVec::<
[Result<ResolvedModuleRecord, Vec<OxcDiagnostic>>; 1],
Expand Down
3 changes: 2 additions & 1 deletion crates/oxc_linter/src/utils/jest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ mod test {
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;

use crate::{ContextHost, ModuleRecord, options::LintOptions};
use crate::{ContextHost, ModuleRecord, frameworks::FrameworkOptions, options::LintOptions};

#[test]
fn test_is_jest_file() {
Expand All @@ -329,6 +329,7 @@ mod test {
Arc::new(ModuleRecord::default()),
LintOptions::default(),
Arc::default(),
FrameworkOptions::None,
))
.spawn_for_test()
};
Expand Down
12 changes: 9 additions & 3 deletions napi/playground/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ use oxc::{
use oxc_formatter::{FormatOptions, Formatter};
use oxc_index::Idx;
use oxc_linter::{
ConfigStore, ConfigStoreBuilder, ExternalPluginStore, LintOptions, Linter, ModuleRecord,
Oxlintrc,
ConfigStore, ConfigStoreBuilder, ExternalPluginStore, FrameworkOptions, LintOptions, Linter,
ModuleRecord, Oxlintrc,
};
use oxc_napi::{Comment, OxcError, convert_utf8_to_utf16};

Expand Down Expand Up @@ -315,7 +315,13 @@ impl Oxc {
ConfigStore::new(lint_config, FxHashMap::default(), ExternalPluginStore::default()),
None,
)
.run(path, Rc::clone(&semantic), Arc::clone(module_record), allocator);
.run(
path,
Rc::clone(&semantic),
Arc::clone(module_record),
FrameworkOptions::None,
allocator,
);
self.diagnostics.extend(linter_ret.into_iter().map(|e| e.error));
}
}
Expand Down
12 changes: 9 additions & 3 deletions tasks/benchmark/benches/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use rustc_hash::FxHashMap;
use oxc_allocator::Allocator;
use oxc_benchmark::{BenchmarkId, Criterion, criterion_group, criterion_main};
use oxc_linter::{
ConfigStore, ConfigStoreBuilder, ExternalPluginStore, FixKind, LintOptions, Linter,
ModuleRecord,
ConfigStore, ConfigStoreBuilder, ExternalPluginStore, FixKind, FrameworkOptions, LintOptions,
Linter, ModuleRecord,
};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
Expand Down Expand Up @@ -39,7 +39,13 @@ fn bench_linter(criterion: &mut Criterion) {
.with_fix(FixKind::All);
group.bench_function(id, |b| {
b.iter(|| {
linter.run(path, Rc::clone(&semantic), Arc::clone(&module_record), &allocator)
linter.run(
path,
Rc::clone(&semantic),
Arc::clone(&module_record),
FrameworkOptions::None,
&allocator,
)
});
});
}
Expand Down
Loading