Skip to content

Commit 077abdf

Browse files
committed
refactor(linter): introduce ContextSubHost for cross script block analyse (#12724)
Another approach to #12541 stack. Because `vue/valid-define-emit` and other rules required the semantics of the other script block. ## The Goal: `Linter.run` should run with the complete file context. In `vue` files and others, there can be multiple Semantics / Module Records. Because `vue` requires some magic compiler rules, the linter should have access to the complete file. This will also fix the offset bug in #12758. ~~**Don't** merge both stacks without rebasing.~~ I do not like the loop part. Maybe you guys have some better ideas. --- <img width="1160" height="422" alt="benchmark results" src="https://github.com/user-attachments/assets/88702a01-eeb2-4a4f-8d80-c5236d3fa8cc" />
1 parent 6431033 commit 077abdf

File tree

7 files changed

+234
-115
lines changed

7 files changed

+234
-115
lines changed

crates/oxc_linter/src/context/host.rs

Lines changed: 97 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,73 @@ 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,
1616
};
1717

1818
use super::{LintContext, plugin_name_to_prefix};
1919

20+
/// Stores shared information about a script block being linted.
21+
pub struct ContextSubHost<'a> {
22+
/// Shared semantic information about the file being linted, which includes scopes, symbols
23+
/// and AST nodes. See [`Semantic`].
24+
pub(super) semantic: Rc<Semantic<'a>>,
25+
/// Cross module information.
26+
pub(super) module_record: Arc<ModuleRecord>,
27+
/// Information about specific rules that should be disabled or enabled, via comment directives like
28+
/// `eslint-disable` or `eslint-disable-next-line`.
29+
pub(super) disable_directives: Rc<DisableDirectives<'a>>,
30+
// Specific framework options, for example, whether the context is inside `<script setup>` in Vue files.
31+
#[expect(dead_code)]
32+
pub(super) framework_options: FrameworkOptions,
33+
/// The source text offset of the sub host
34+
#[expect(dead_code)]
35+
pub(super) source_text_offset: u32,
36+
}
37+
38+
impl<'a> ContextSubHost<'a> {
39+
pub fn new(
40+
semantic: Rc<Semantic<'a>>,
41+
module_record: Arc<ModuleRecord>,
42+
source_text_offset: u32,
43+
) -> Self {
44+
Self::new_with_framework_options(
45+
semantic,
46+
module_record,
47+
source_text_offset,
48+
FrameworkOptions::Default,
49+
)
50+
}
51+
52+
/// # Panics
53+
/// If `semantic.cfg()` is `None`.
54+
pub fn new_with_framework_options(
55+
semantic: Rc<Semantic<'a>>,
56+
module_record: Arc<ModuleRecord>,
57+
source_text_offset: u32,
58+
frameworks_options: FrameworkOptions,
59+
) -> Self {
60+
// We should always check for `semantic.cfg()` being `Some` since we depend on it and it is
61+
// unwrapped without any runtime checks after construction.
62+
assert!(
63+
semantic.cfg().is_some(),
64+
"`LintContext` depends on `Semantic::cfg`, Build your semantic with cfg enabled(`SemanticBuilder::with_cfg`)."
65+
);
66+
67+
let disable_directives =
68+
DisableDirectivesBuilder::new().build(semantic.source_text(), semantic.comments());
69+
70+
Self {
71+
semantic,
72+
module_record,
73+
source_text_offset,
74+
disable_directives: Rc::new(disable_directives),
75+
framework_options: frameworks_options,
76+
}
77+
}
78+
}
2079
/// Stores shared information about a file being linted.
2180
///
2281
/// When linting a file, there are a number of shared resources that are
@@ -38,14 +97,11 @@ use super::{LintContext, plugin_name_to_prefix};
3897
#[must_use]
3998
#[non_exhaustive]
4099
pub struct ContextHost<'a> {
41-
/// Shared semantic information about the file being linted, which includes scopes, symbols
42-
/// and AST nodes. See [`Semantic`].
43-
pub(super) semantic: Rc<Semantic<'a>>,
44-
/// Cross module information.
45-
pub(super) module_record: Arc<ModuleRecord>,
46-
/// Information about specific rules that should be disabled or enabled, via comment directives like
47-
/// `eslint-disable` or `eslint-disable-next-line`.
48-
pub(super) disable_directives: DisableDirectives<'a>,
100+
/// A file can have multiple script entries.
101+
/// Some rules (like vue) need the information of the other entries.
102+
pub(super) sub_hosts: Vec<ContextSubHost<'a>>,
103+
/// The current index which will be linted.
104+
current_sub_host_index: RefCell<usize>,
49105
/// Diagnostics reported by the linter.
50106
///
51107
/// Contains diagnostics for all rules across a single file.
@@ -67,32 +123,25 @@ pub struct ContextHost<'a> {
67123

68124
impl<'a> ContextHost<'a> {
69125
/// # Panics
70-
/// If `semantic.cfg()` is `None`.
126+
/// If `sub_hosts` is empty.
71127
pub fn new<P: AsRef<Path>>(
72128
file_path: P,
73-
semantic: Rc<Semantic<'a>>,
74-
module_record: Arc<ModuleRecord>,
129+
sub_hosts: Vec<ContextSubHost<'a>>,
75130
options: LintOptions,
76131
config: Arc<LintConfig>,
77132
) -> Self {
78133
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 512;
79134

80-
// We should always check for `semantic.cfg()` being `Some` since we depend on it and it is
81-
// unwrapped without any runtime checks after construction.
82135
assert!(
83-
semantic.cfg().is_some(),
84-
"`LintContext` depends on `Semantic::cfg`, Build your semantic with cfg enabled(`SemanticBuilder::with_cfg`)."
136+
!sub_hosts.is_empty(),
137+
"ContextHost requires at least one ContextSubHost to be analyzed"
85138
);
86139

87-
let disable_directives =
88-
DisableDirectivesBuilder::new().build(semantic.source_text(), semantic.comments());
89-
90140
let file_path = file_path.as_ref().to_path_buf().into_boxed_path();
91141

92142
Self {
93-
semantic,
94-
module_record,
95-
disable_directives,
143+
sub_hosts,
144+
current_sub_host_index: RefCell::new(0),
96145
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),
97146
fix: options.fix,
98147
file_path,
@@ -102,16 +151,26 @@ impl<'a> ContextHost<'a> {
102151
.sniff_for_frameworks()
103152
}
104153

105-
/// Shared reference to the [`Semantic`] analysis of the file.
154+
/// The current [`ContextSubHost`]
155+
fn current_sub_host(&self) -> &ContextSubHost<'a> {
156+
&self.sub_hosts[*self.current_sub_host_index.borrow()]
157+
}
158+
159+
/// Shared reference to the [`Semantic`] analysis of current script block.
106160
#[inline]
107-
pub fn semantic(&self) -> &Semantic<'a> {
108-
&self.semantic
161+
pub fn semantic(&self) -> &Rc<Semantic<'a>> {
162+
&self.current_sub_host().semantic
109163
}
110164

111-
/// Shared reference to the [`ModuleRecord`] of the file.
165+
/// Shared reference to the [`ModuleRecord`] of the current script block.
112166
#[inline]
113167
pub fn module_record(&self) -> &ModuleRecord {
114-
&self.module_record
168+
&self.current_sub_host().module_record
169+
}
170+
171+
/// Shared reference to the [`DisableDirectives`] of the current script block.
172+
pub fn disable_directives(&self) -> &Rc<DisableDirectives<'a>> {
173+
&self.current_sub_host().disable_directives
115174
}
116175

117176
/// Path to the file being linted.
@@ -127,7 +186,7 @@ impl<'a> ContextHost<'a> {
127186
/// CJS, ESM, etc.
128187
#[inline]
129188
pub fn source_type(&self) -> &SourceType {
130-
self.semantic.source_type()
189+
self.semantic().source_type()
131190
}
132191

133192
#[inline]
@@ -147,14 +206,20 @@ impl<'a> ContextHost<'a> {
147206
self.diagnostics.borrow_mut().extend(diagnostics);
148207
}
149208

209+
// move the context to the next sub host
210+
pub fn next_sub_host(&self) -> bool {
211+
*self.current_sub_host_index.borrow_mut() += 1;
212+
self.sub_hosts.get(*self.current_sub_host_index.borrow()).is_some()
213+
}
214+
150215
/// report unused enable/disable directives, add these as Messages to diagnostics
151216
pub fn report_unused_directives(&self, rule_severity: Severity) {
152217
// report unused disable
153218
// relate to lint result, check after linter run finish
154-
let unused_disable_comments = self.disable_directives.collect_unused_disable_comments();
219+
let unused_disable_comments = self.disable_directives().collect_unused_disable_comments();
155220
let message_for_disable = "Unused eslint-disable directive (no problems were reported).";
156221
let fix_message = "remove unused disable directive";
157-
let source_text = self.semantic.source_text();
222+
let source_text = self.semantic().source_text();
158223

159224
for unused_disable_comment in unused_disable_comments {
160225
let span = unused_disable_comment.span;
@@ -188,14 +253,14 @@ impl<'a> ContextHost<'a> {
188253
}
189254
}
190255

191-
let unused_enable_comments = self.disable_directives.unused_enable_comments();
256+
let unused_enable_comments = self.disable_directives().unused_enable_comments();
192257
let mut unused_directive_diagnostics: Vec<(Cow<str>, Span)> =
193258
Vec::with_capacity(unused_enable_comments.len());
194259
// report unused enable
195260
// not relate to lint result, check during comment directives' construction
196261
let message_for_enable =
197262
"Unused eslint-enable directive (no matching eslint-disable directives were found).";
198-
for (rule_name, enable_comment_span) in self.disable_directives.unused_enable_comments() {
263+
for (rule_name, enable_comment_span) in self.disable_directives().unused_enable_comments() {
199264
unused_directive_diagnostics.push((
200265
rule_name.map_or(Cow::Borrowed(message_for_enable), |name| {
201266
Cow::Owned(format!(

crates/oxc_linter/src/context/mod.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
};
2121

2222
mod host;
23-
pub use host::ContextHost;
23+
pub use host::{ContextHost, ContextSubHost};
2424

2525
/// Contains all of the state and context specific to this lint rule.
2626
///
@@ -106,7 +106,7 @@ impl<'a> LintContext<'a> {
106106
/// Refer to [`Semantic`]'s documentation for more information.
107107
#[inline]
108108
pub fn semantic(&self) -> &Rc<Semantic<'a>> {
109-
&self.parent.semantic
109+
self.parent.semantic()
110110
}
111111

112112
#[inline]
@@ -119,19 +119,19 @@ impl<'a> LintContext<'a> {
119119
pub fn cfg(&self) -> &ControlFlowGraph {
120120
// SAFETY: `LintContext::new` is the only way to construct a `LintContext` and we always
121121
// assert the existence of control flow so it should always be `Some`.
122-
unsafe { self.parent.semantic.cfg().unwrap_unchecked() }
122+
unsafe { self.parent.semantic().cfg().unwrap_unchecked() }
123123
}
124124

125125
/// List of all disable directives in the file being linted.
126126
#[inline]
127-
pub fn disable_directives(&self) -> &DisableDirectives<'a> {
128-
&self.parent.disable_directives
127+
pub fn disable_directives(&self) -> &Rc<DisableDirectives<'a>> {
128+
self.parent.disable_directives()
129129
}
130130

131131
/// Get a snippet of source text covered by the given [`Span`]. For details,
132132
/// see [`Span::source_text`].
133133
pub fn source_range(&self, span: Span) -> &'a str {
134-
span.source_text(self.parent.semantic.source_text())
134+
span.source_text(self.parent.semantic().source_text())
135135
}
136136

137137
/// Path to the file currently being linted.
@@ -222,7 +222,7 @@ impl<'a> LintContext<'a> {
222222
/// Add a diagnostic message to the list of diagnostics. Outputs a diagnostic with the current rule
223223
/// name, severity, and a link to the rule's documentation URL.
224224
fn add_diagnostic(&self, mut message: Message<'a>) {
225-
if self.parent.disable_directives.contains(self.current_rule_name, message.span()) {
225+
if self.parent.disable_directives().contains(self.current_rule_name, message.span()) {
226226
return;
227227
}
228228
message.error = message

crates/oxc_linter/src/frameworks.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,10 @@ 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+
101+
pub enum FrameworkOptions {
102+
Default, // default
103+
VueSetup, // context is inside `<script setup>`
104+
}

0 commit comments

Comments
 (0)