Skip to content

Commit 8f63297

Browse files
committed
refactor(linter): add setup detection for vue files
1 parent c290eae commit 8f63297

File tree

12 files changed

+96
-21
lines changed

12 files changed

+96
-21
lines changed

crates/oxc_linter/src/context/host.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
config::LintConfig,
1010
disable_directives::{DisableDirectives, DisableDirectivesBuilder, RuleCommentType},
1111
fixer::{Fix, FixKind, Message, PossibleFixes},
12-
frameworks,
12+
frameworks::{self, FrameworkOptions},
1313
module_record::ModuleRecord,
1414
options::LintOptions,
1515
rules::RuleEnum,
@@ -63,6 +63,8 @@ pub struct ContextHost<'a> {
6363
pub(super) config: Arc<LintConfig>,
6464
/// Front-end frameworks that might be in use in the target file.
6565
pub(super) frameworks: FrameworkFlags,
66+
// Specific framework options, for example, whether the context is inside `<script setup>` in Vue files.
67+
pub(super) frameworks_options: FrameworkOptions,
6668
}
6769

6870
impl<'a> ContextHost<'a> {
@@ -74,6 +76,7 @@ impl<'a> ContextHost<'a> {
7476
module_record: Arc<ModuleRecord>,
7577
options: LintOptions,
7678
config: Arc<LintConfig>,
79+
frameworks_options: FrameworkOptions,
7780
) -> Self {
7881
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 512;
7982

@@ -98,6 +101,7 @@ impl<'a> ContextHost<'a> {
98101
file_path,
99102
config,
100103
frameworks: options.framework_hints,
104+
frameworks_options,
101105
}
102106
.sniff_for_frameworks()
103107
}

crates/oxc_linter/src/context/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::{
1717
config::GlobalValue,
1818
disable_directives::DisableDirectives,
1919
fixer::{Fix, FixKind, Message, PossibleFixes, RuleFix, RuleFixer},
20+
frameworks::FrameworkOptions,
2021
};
2122

2223
mod host;
@@ -114,6 +115,10 @@ impl<'a> LintContext<'a> {
114115
self.parent.module_record()
115116
}
116117

118+
pub fn frameworks_options(&self) -> &FrameworkOptions {
119+
&self.parent.frameworks_options
120+
}
121+
117122
/// Get the control flow graph for the current program.
118123
#[inline]
119124
pub fn cfg(&self) -> &ControlFlowGraph {

crates/oxc_linter/src/frameworks.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,9 @@ pub fn has_vitest_imports(module_record: &ModuleRecord) -> bool {
9595
pub fn has_jest_imports(module_record: &ModuleRecord) -> bool {
9696
module_record.import_entries.iter().any(|entry| entry.module_request.name() == "@jest/globals")
9797
}
98+
99+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
100+
pub enum FrameworkOptions {
101+
None, // default
102+
VueSetup, // context is inside `<script setup>`
103+
}

crates/oxc_linter/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub use crate::{
4949
},
5050
external_plugin_store::{ExternalPluginStore, ExternalRuleId},
5151
fixer::FixKind,
52-
frameworks::FrameworkFlags,
52+
frameworks::{FrameworkFlags, FrameworkOptions},
5353
loader::LINTABLE_EXTENSIONS,
5454
module_record::ModuleRecord,
5555
options::LintOptions,
@@ -126,12 +126,19 @@ impl Linter {
126126
path: &Path,
127127
semantic: Rc<Semantic<'a>>,
128128
module_record: Arc<ModuleRecord>,
129+
framework_options: FrameworkOptions,
129130
allocator: &Allocator,
130131
) -> Vec<Message<'a>> {
131132
let ResolvedLinterState { rules, config, external_rules } = self.config.resolve(path);
132133

133-
let ctx_host =
134-
Rc::new(ContextHost::new(path, semantic, module_record, self.options, config));
134+
let ctx_host = Rc::new(ContextHost::new(
135+
path,
136+
semantic,
137+
module_record,
138+
self.options,
139+
config,
140+
framework_options,
141+
));
135142

136143
let rules = rules
137144
.iter()

crates/oxc_linter/src/loader/partial_loader/astro.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use memchr::memmem::Finder;
22

33
use oxc_span::{SourceType, Span};
44

5-
use crate::loader::JavaScriptSource;
5+
use crate::{frameworks::FrameworkOptions, loader::JavaScriptSource};
66

77
use super::{SCRIPT_END, SCRIPT_START};
88

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

5353
/// In .astro files, you can add client-side JavaScript by adding one (or more) `<script>` tags.
@@ -94,6 +94,7 @@ impl<'a> AstroPartialLoader<'a> {
9494
results.push(JavaScriptSource::partial(
9595
&self.source_text[js_start..js_end],
9696
SourceType::ts(),
97+
FrameworkOptions::None,
9798
js_start as u32,
9899
));
99100
}

crates/oxc_linter/src/loader/partial_loader/svelte.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use memchr::memmem::Finder;
22

33
use oxc_span::SourceType;
44

5-
use crate::loader::JavaScriptSource;
5+
use crate::{frameworks::FrameworkOptions, loader::JavaScriptSource};
66

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

@@ -62,7 +62,12 @@ impl<'a> SveltePartialLoader<'a> {
6262

6363
// NOTE: loader checked that source_text.len() is less than u32::MAX
6464
#[expect(clippy::cast_possible_truncation)]
65-
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
65+
Some(JavaScriptSource::partial(
66+
source_text,
67+
source_type,
68+
FrameworkOptions::None,
69+
js_start as u32,
70+
))
6671
}
6772
}
6873

crates/oxc_linter/src/loader/partial_loader/vue.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use memchr::memmem::Finder;
22

33
use oxc_span::SourceType;
44

5+
use crate::frameworks::FrameworkOptions;
6+
57
use super::{JavaScriptSource, SCRIPT_END, SCRIPT_START, find_script_closing_angle};
68

79
pub struct VuePartialLoader<'a> {
@@ -52,6 +54,7 @@ impl<'a> VuePartialLoader<'a> {
5254

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

5659
let Ok(mut source_type) = SourceType::from_extension(lang) else { return None };
5760
if !lang.contains('x') {
@@ -70,7 +73,12 @@ impl<'a> VuePartialLoader<'a> {
7073
let source_text = &self.source_text[js_start..js_end];
7174
// NOTE: loader checked that source_text.len() is less than u32::MAX
7275
#[expect(clippy::cast_possible_truncation)]
73-
Some(JavaScriptSource::partial(source_text, source_type, js_start as u32))
76+
Some(JavaScriptSource::partial(
77+
source_text,
78+
source_type,
79+
if is_setup { FrameworkOptions::VueSetup } else { FrameworkOptions::None },
80+
js_start as u32,
81+
))
7482
}
7583

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

126+
use crate::FrameworkOptions;
127+
118128
use super::{JavaScriptSource, VuePartialLoader};
119129

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

134144
let result = parse_vue(source_text);
145+
assert_eq!(result.framework_options, FrameworkOptions::None);
135146
assert_eq!(result.source_text, r#" console.log("hi") "#);
136147
}
137148

@@ -229,7 +240,9 @@ mod test {
229240
let sources = VuePartialLoader::new(source_text).parse();
230241
assert_eq!(sources.len(), 2);
231242
assert_eq!(sources[0].source_text, "a");
243+
assert_eq!(sources[0].framework_options, FrameworkOptions::None);
232244
assert_eq!(sources[1].source_text, "b");
245+
assert_eq!(sources[1].framework_options, FrameworkOptions::VueSetup);
233246
}
234247

235248
#[test]

crates/oxc_linter/src/loader/source.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use oxc_span::SourceType;
22

3+
use crate::frameworks::FrameworkOptions;
4+
35
#[derive(Debug, Clone, Copy)]
46
#[non_exhaustive]
57
pub struct JavaScriptSource<'a> {
@@ -10,15 +12,29 @@ pub struct JavaScriptSource<'a> {
1012
pub start: u32,
1113
#[expect(dead_code)]
1214
is_partial: bool,
15+
16+
// some partial sources can have special options defined, like Vue's `<script setup>`.
17+
pub framework_options: FrameworkOptions,
1318
}
1419

1520
impl<'a> JavaScriptSource<'a> {
1621
pub fn new(source_text: &'a str, source_type: SourceType) -> Self {
17-
Self { source_text, source_type, start: 0, is_partial: false }
22+
Self {
23+
source_text,
24+
source_type,
25+
start: 0,
26+
is_partial: false,
27+
framework_options: FrameworkOptions::None,
28+
}
1829
}
1930

20-
pub fn partial(source_text: &'a str, source_type: SourceType, start: u32) -> Self {
21-
Self { source_text, source_type, start, is_partial: true }
31+
pub fn partial(
32+
source_text: &'a str,
33+
source_type: SourceType,
34+
framework_options: FrameworkOptions,
35+
start: u32,
36+
) -> Self {
37+
Self { source_text, source_type, start, is_partial: true, framework_options }
2238
}
2339

2440
pub fn as_str(&self) -> &'a str {

crates/oxc_linter/src/service/runtime.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use oxc_span::{CompactStr, SourceType, VALID_EXTENSIONS};
2525
use crate::{
2626
Fixer, Linter, Message,
2727
fixer::PossibleFixes,
28+
frameworks::FrameworkOptions,
2829
loader::{JavaScriptSource, LINT_PARTIAL_LOADER_EXTENSIONS, PartialLoader},
2930
module_record::ModuleRecord,
3031
utils::read_to_arena_str,
@@ -524,6 +525,7 @@ impl Runtime {
524525
path,
525526
Rc::new(section.semantic.unwrap()),
526527
Arc::clone(&module_record),
528+
section.source.framework_options,
527529
allocator_guard,
528530
),
529531
Err(errors) => errors
@@ -635,6 +637,7 @@ impl Runtime {
635637
Path::new(&module.path),
636638
Rc::new(section.semantic.unwrap()),
637639
Arc::clone(&module_record),
640+
section.source.framework_options,
638641
allocator_guard,
639642
);
640643

@@ -765,6 +768,7 @@ impl Runtime {
765768
Path::new(&module.path),
766769
Rc::new(section.semantic.unwrap()),
767770
Arc::clone(&module_record),
771+
section.source.framework_options,
768772
allocator_guard,
769773
),
770774
Err(errors) => errors
@@ -890,8 +894,9 @@ impl Runtime {
890894
allocator: &'a Allocator,
891895
mut out_sections: Option<&mut SectionContents<'a>>,
892896
) -> SmallVec<[Result<ResolvedModuleRecord, Vec<OxcDiagnostic>>; 1]> {
893-
let section_sources = PartialLoader::parse(ext, source_text)
894-
.unwrap_or_else(|| vec![JavaScriptSource::partial(source_text, source_type, 0)]);
897+
let section_sources = PartialLoader::parse(ext, source_text).unwrap_or_else(|| {
898+
vec![JavaScriptSource::partial(source_text, source_type, FrameworkOptions::None, 0)]
899+
});
895900

896901
let mut section_module_records = SmallVec::<
897902
[Result<ResolvedModuleRecord, Vec<OxcDiagnostic>>; 1],

crates/oxc_linter/src/utils/jest.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ mod test {
311311
use oxc_semantic::SemanticBuilder;
312312
use oxc_span::SourceType;
313313

314-
use crate::{ContextHost, ModuleRecord, options::LintOptions};
314+
use crate::{ContextHost, ModuleRecord, frameworks::FrameworkOptions, options::LintOptions};
315315

316316
#[test]
317317
fn test_is_jest_file() {
@@ -329,6 +329,7 @@ mod test {
329329
Arc::new(ModuleRecord::default()),
330330
LintOptions::default(),
331331
Arc::default(),
332+
FrameworkOptions::None,
332333
))
333334
.spawn_for_test()
334335
};

0 commit comments

Comments
 (0)