Skip to content

Commit bcd6508

Browse files
authored
refactor(core): semantic model in collector (#9905)
1 parent 89c3e32 commit bcd6508

42 files changed

Lines changed: 920 additions & 543 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/correctness/no_unused_imports.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,8 @@ impl Visitor for JsDocTypeCollectorVisitor {
161161
}
162162

163163
fn load_jsdoc_types_from_node(model: &mut JsDocTypeModel, node: &SyntaxNode<JsLanguage>) {
164-
JsdocComment::for_each(node, |comment| {
165-
load_jsdoc_types_from_jsdoc_comment(model, comment)
166-
});
164+
JsdocComment::get_jsdocs(node)
165+
.for_each(|comment| load_jsdoc_types_from_jsdoc_comment(model, comment.as_str()));
167166
}
168167

169168
static JSDOC_INLINE_TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| {

crates/biome_js_semantic/Cargo.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,19 @@ categories.workspace = true
1212
publish = true
1313

1414
[dependencies]
15-
biome_formatter = { workspace = true }
16-
biome_js_syntax = { workspace = true }
17-
biome_rowan = { workspace = true }
18-
rust-lapper = { workspace = true }
19-
rustc-hash = { workspace = true }
20-
smallvec = { workspace = true }
15+
biome_formatter = { workspace = true }
16+
biome_js_syntax = { workspace = true }
17+
biome_jsdoc_comment = { workspace = true }
18+
biome_rowan = { workspace = true }
19+
rust-lapper = { workspace = true }
20+
rustc-hash = { workspace = true }
21+
smallvec = { workspace = true }
2122

2223
[dev-dependencies]
2324
biome_console = { path = "../biome_console" }
2425
biome_diagnostics = { path = "../biome_diagnostics" }
2526
biome_js_parser = { path = "../biome_js_parser" }
27+
insta = { workspace = true }
2628

2729
[lints]
2830
workspace = true

crates/biome_js_semantic/src/format_semantic_model.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::{Binding, BindingId, JsDeclarationKind, Scope, ScopeId, SemanticModel};
12
use biome_formatter::prelude::*;
23
use biome_formatter::{
34
FormatContext, FormatOptions, IndentStyle, IndentWidth, LineEnding, LineWidth,
@@ -6,8 +7,6 @@ use biome_formatter::{
67
use biome_formatter::{format_args, write};
78
use biome_js_syntax::TextSize;
89

9-
use crate::{Binding, BindingId, Scope, ScopeId, SemanticModel};
10-
1110
pub struct FormatSemanticModelOptions;
1211

1312
impl FormatOptions for FormatSemanticModelOptions {
@@ -141,19 +140,41 @@ impl Format<FormatSemanticModelContext> for Scope {
141140

142141
impl Format<FormatSemanticModelContext> for Binding {
143142
fn fmt(&self, f: &mut Formatter<FormatSemanticModelContext>) -> FormatResult<()> {
143+
let is_imported = format_with(|f| {
144+
if self.is_imported() {
145+
write!(f, [token("Imported: true"), hard_line_break()])
146+
} else {
147+
write!(f, [token("Imported: false"), hard_line_break()])
148+
}
149+
});
150+
151+
let is_exported = format_with(|f| {
152+
if self.is_exported() {
153+
write!(f, [token("Exported: true"), hard_line_break()])
154+
} else {
155+
write!(f, [token("Exported: false"), hard_line_break()])
156+
}
157+
});
158+
144159
let formatted_binding_info = format_with(|f| {
145160
let range = std::format!("{:?}", self.syntax().text_trimmed_range());
161+
146162
write!(
147163
f,
148164
[
149-
token("id: "),
165+
token("Id: "),
150166
self.id,
151167
token(" @ "),
152168
text(range.as_str(), TextSize::default()),
153169
hard_line_break()
154170
]
155171
)?;
156-
write!(f, [token("scope: "), self.scope().id, hard_line_break()])?;
172+
write!(f, [token("Scope: "), self.scope().id, hard_line_break()])?;
173+
write!(f, [is_imported, is_exported])?;
174+
write!(
175+
f,
176+
[token("Kind: "), self.declaration_kind(), hard_line_break()]
177+
)?;
157178
let full_text = self.syntax().text_trimmed().into_text();
158179
write!(
159180
f,
@@ -163,6 +184,17 @@ impl Format<FormatSemanticModelContext> for Binding {
163184
hard_line_break()
164185
]
165186
)?;
187+
188+
if let Some(jsdoc) = self.to_fmt_jsonc() {
189+
write!(
190+
f,
191+
[
192+
token("JsDoc Comments: "),
193+
text(jsdoc.as_str(), TextSize::default())
194+
]
195+
)?;
196+
}
197+
166198
Ok(())
167199
});
168200
write!(
@@ -192,3 +224,10 @@ impl Format<FormatSemanticModelContext> for BindingId {
192224
write!(f, [text(binding_text.as_str(), TextSize::default())])
193225
}
194226
}
227+
228+
impl Format<FormatSemanticModelContext> for JsDeclarationKind {
229+
fn fmt(&self, f: &mut Formatter<FormatSemanticModelContext>) -> FormatResult<()> {
230+
let text_kind = std::format!("{:?}", self);
231+
write!(f, [text(text_kind.as_str(), TextSize::default())])
232+
}
233+
}

crates/biome_js_semantic/src/semantic_model/binding.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use biome_js_syntax::{
44
AnyJsDeclaration, JsImport, JsVariableKind, TextRange, TsTypeParameter, TsTypeParameterName,
55
binding_ext::AnyJsIdentifierBinding,
66
};
7+
use biome_jsdoc_comment::JsdocComment;
78
use std::fmt::{Display, Formatter};
89
use std::sync::Arc;
910

@@ -457,9 +458,11 @@ pub(crate) struct SemanticModelBindingData {
457458
pub(crate) range: TextRange,
458459
pub(crate) references: Vec<SemanticModelReference>,
459460
// We use a SmallVec because most of the time a binding is expected once.
460-
pub(crate) export_by_start: smallvec::SmallVec<[TextSize; 4]>,
461+
pub(crate) export_ranges: smallvec::SmallVec<[TextRange; 4]>,
461462
/// The kind of declaration that introduced this binding.
462463
pub(crate) declaration_kind: JsDeclarationKind,
464+
/// JSDoc comment associated with the binding.
465+
pub(crate) jsdoc: Option<JsdocComment>,
463466
}
464467

465468
#[derive(Clone, Copy, Debug)]
@@ -596,14 +599,39 @@ impl Binding {
596599
/// itself an `export` statement) or an identifier usage.
597600
pub fn exports(&self) -> impl Iterator<Item = JsSyntaxNode> + '_ {
598601
let binding = self.data.binding(self.id);
599-
binding.export_by_start.iter().map(|export_start| {
600-
self.data.binding_node_by_start[export_start].to_node(self.data.to_root().syntax())
602+
binding.export_ranges.iter().filter_map(|export_start| {
603+
self.data
604+
.binding_node_by_start
605+
.get(&export_start.start())
606+
.map(|ptr| ptr.to_node(self.data.to_root().syntax()))
601607
})
602608
}
603609

604610
pub fn is_imported(&self) -> bool {
605611
super::is_imported(&self.syntax())
606612
}
613+
614+
pub fn is_exported(&self) -> bool {
615+
self.data.is_exported(self.syntax().text_trimmed_range())
616+
}
617+
618+
/// Returns the JSDoc comment associated with this binding, if any.
619+
pub fn jsdoc(&self) -> Option<&JsdocComment> {
620+
let binding = self.data.binding(self.id);
621+
binding.jsdoc.as_ref()
622+
}
623+
624+
/// Returns the formatted JsDoc comment
625+
pub fn to_fmt_jsonc(&self) -> Option<String> {
626+
let binding = self.data.binding(self.id);
627+
binding.jsdoc.clone().map(|jsdoc| jsdoc.to_string())
628+
}
629+
630+
/// Returns the text ranges of all export sites for this binding.
631+
pub fn export_ranges(&self) -> &[TextRange] {
632+
let binding = self.data.binding(self.id);
633+
binding.export_ranges.as_slice()
634+
}
607635
}
608636

609637
impl PartialEq for Binding {

crates/biome_js_semantic/src/semantic_model/builder.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use super::*;
2-
use biome_js_syntax::{AnyJsRoot, JsSyntaxNode, TextRange, TsConditionalType, TsTypeParameterName};
2+
use biome_js_syntax::{
3+
AnyJsDeclaration, AnyJsRoot, JsExport, JsSyntaxNode, TextRange, TsConditionalType,
4+
TsTypeParameterName,
5+
};
6+
use biome_jsdoc_comment::JsdocComment;
37
use biome_rowan::SyntaxNodePtr;
48
use rustc_hash::{FxHashMap, FxHashSet};
59
use std::collections::hash_map::Entry;
@@ -27,6 +31,7 @@ pub struct SemanticModelBuilder {
2731
declared_at_by_start: FxHashMap<TextSize, BindingId>,
2832
exported: FxHashSet<TextSize>,
2933
unresolved_references: Vec<SemanticModelUnresolvedReference>,
34+
pub(crate) export_jsdoc_by_range: FxHashMap<TextRange, JsdocComment>,
3035
}
3136

3237
impl SemanticModelBuilder {
@@ -45,6 +50,7 @@ impl SemanticModelBuilder {
4550
declared_at_by_start: FxHashMap::default(),
4651
exported: FxHashSet::default(),
4752
unresolved_references: Vec::new(),
53+
export_jsdoc_by_range: FxHashMap::default(),
4854
}
4955
}
5056

@@ -113,6 +119,12 @@ impl SemanticModelBuilder {
113119
self.scope_node_by_range
114120
.insert(node.text_trimmed_range(), node.clone());
115121
}
122+
JS_EXPORT => {
123+
if let Ok(jsdoc) = JsdocComment::try_from(node) {
124+
self.export_jsdoc_by_range
125+
.insert(node.text_trimmed_range(), jsdoc);
126+
}
127+
}
116128
_ => {
117129
if let Some(conditional_type) = TsConditionalType::cast_ref(node)
118130
&& let Ok(conditional_true_type) = conditional_type.true_type()
@@ -181,11 +193,16 @@ impl SemanticModelBuilder {
181193
debug_assert!((binding_scope_id.index()) < self.scopes.len());
182194

183195
let binding_id = BindingId::new(self.bindings.len());
196+
let jsdoc = self
197+
.binding_node_by_start
198+
.get(&range.start())
199+
.and_then(find_jsdoc);
184200
self.bindings.push(SemanticModelBindingData {
185201
range,
186202
references: Vec::new(),
187-
export_by_start: smallvec::SmallVec::new(),
203+
export_ranges: smallvec::SmallVec::new(),
188204
declaration_kind,
205+
jsdoc,
189206
});
190207
self.bindings_by_start.insert(range.start(), binding_id);
191208

@@ -345,7 +362,7 @@ impl SemanticModelBuilder {
345362

346363
let binding_id = self.bindings_by_start[&declaration_at];
347364
let binding = &mut self.bindings[binding_id.index()];
348-
binding.export_by_start.push(range.start());
365+
binding.export_ranges.push(range);
349366
}
350367
}
351368
}
@@ -379,7 +396,20 @@ impl SemanticModelBuilder {
379396
exported: self.exported,
380397
unresolved_references: self.unresolved_references,
381398
globals: self.globals,
399+
export_jsdoc_by_range: self.export_jsdoc_by_range,
382400
};
383401
SemanticModel::new(data)
384402
}
385403
}
404+
405+
fn find_jsdoc(node: &JsSyntaxNode) -> Option<JsdocComment> {
406+
node.ancestors().find_map(|ancestor| {
407+
if let Some(export) = JsExport::cast_ref(&ancestor) {
408+
JsdocComment::try_from(export.syntax()).ok()
409+
} else if let Some(decl) = AnyJsDeclaration::cast(ancestor) {
410+
JsdocComment::try_from(decl.syntax()).ok()
411+
} else {
412+
None
413+
}
414+
})
415+
}

crates/biome_js_semantic/src/semantic_model/model.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::*;
22
use biome_js_syntax::{AnyJsFunction, AnyJsRoot, JsSyntaxNodePtr};
3+
use biome_jsdoc_comment::JsdocComment;
34
use biome_rowan::SendNode;
45
use std::sync::Arc;
56

@@ -93,6 +94,8 @@ pub(crate) struct SemanticModelData {
9394
pub(crate) unresolved_references: Vec<SemanticModelUnresolvedReference>,
9495
/// All globals references
9596
pub(crate) globals: Vec<SemanticModelGlobalBindingData>,
97+
/// JSDoc comments attached to export statements (keyed by the JsExport node's range).
98+
pub(crate) export_jsdoc_by_range: FxHashMap<TextRange, JsdocComment>,
9699
}
97100

98101
impl SemanticModelData {
@@ -513,4 +516,27 @@ impl SemanticModel {
513516
.all_reads(self),
514517
})
515518
}
519+
520+
/// Returns a [Binding] for the declaration at the given range if one exists.
521+
pub fn as_binding_by_range(&self, range: TextRange) -> Option<Binding> {
522+
let binding_id = self.data.bindings_by_start.get(&range.start())?;
523+
Some(Binding {
524+
data: self.data.clone(),
525+
id: *binding_id,
526+
})
527+
}
528+
529+
/// Returns a [Binding] for the declaration at the given start range
530+
pub fn as_binding_by_range_start(&self, range: TextSize) -> Option<Binding> {
531+
let binding_id = self.data.bindings_by_start.get(&range)?;
532+
Some(Binding {
533+
data: self.data.clone(),
534+
id: *binding_id,
535+
})
536+
}
537+
538+
/// Returns the JSDoc comment attached to the export statement at `range`, if any.
539+
pub fn export_jsdoc(&self, range: TextRange) -> Option<&JsdocComment> {
540+
self.data.export_jsdoc_by_range.get(&range)
541+
}
516542
}

0 commit comments

Comments
 (0)