diff --git a/Cargo.lock b/Cargo.lock index 14ae537309cb..62bcc4b1d523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,6 +534,16 @@ dependencies = [ "unicode-bom", ] +[[package]] +name = "biome_graphql_semantic" +version = "0.0.0" +dependencies = [ + "biome_graphql_parser", + "biome_graphql_syntax", + "biome_rowan", + "rustc-hash", +] + [[package]] name = "biome_graphql_syntax" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 876689f41fbb..36a21318e8d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ biome_graphql_analyze = { version = "0.0.1", path = "./crates/biome_graph biome_graphql_factory = { version = "0.1.0", path = "./crates/biome_graphql_factory" } biome_graphql_formatter = { version = "0.1.0", path = "./crates/biome_graphql_formatter" } biome_graphql_parser = { version = "0.1.0", path = "./crates/biome_graphql_parser" } +biome_graphql_semantic = { version = "0.0.0", path = "./crates/biome_graphql_semantic" } biome_graphql_syntax = { version = "0.1.0", path = "./crates/biome_graphql_syntax" } biome_grit_factory = { version = "0.5.7", path = "./crates/biome_grit_factory" } biome_grit_formatter = { version = "0.0.0", path = "./crates/biome_grit_formatter" } diff --git a/crates/biome_graphql_semantic/Cargo.toml b/crates/biome_graphql_semantic/Cargo.toml new file mode 100644 index 000000000000..ba75ef4c30b5 --- /dev/null +++ b/crates/biome_graphql_semantic/Cargo.toml @@ -0,0 +1,23 @@ + +[package] +authors.workspace = true +categories.workspace = true +description = "Biome's semantic model for GraphQL" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "biome_graphql_semantic" +repository.workspace = true +version = "0.0.0" + +[lints] +workspace = true + +[dependencies] +biome_graphql_syntax = { workspace = true } +biome_rowan = { workspace = true } +rustc-hash = { workspace = true } + +[dev-dependencies] +biome_graphql_parser = { path = "../biome_graphql_parser" } diff --git a/crates/biome_graphql_semantic/src/events.rs b/crates/biome_graphql_semantic/src/events.rs new file mode 100644 index 000000000000..e218256917df --- /dev/null +++ b/crates/biome_graphql_semantic/src/events.rs @@ -0,0 +1,488 @@ +//! Events emitted by the [SemanticEventExtractor] which are then constructed into the Semantic Model + +use biome_graphql_syntax::{ + AnyGraphqlTypeDefinition, AnyGraphqlTypeExtension, GraphqlFragmentDefinition, + GraphqlNameBinding, GraphqlNameReference, GraphqlOperationDefinition, GraphqlSyntaxKind, + GraphqlSyntaxNode, GraphqlVariableBinding, GraphqlVariableReference, TextRange, +}; +use biome_rowan::{AstNode, TokenText}; +use rustc_hash::FxHashMap; +use std::collections::{HashSet, VecDeque}; +use GraphqlSyntaxKind::*; + +/// Events emitted by the [SemanticEventExtractor]. +/// These events are later made into the Semantic Model. +#[derive(Debug, Eq, PartialEq)] +pub enum SemanticEvent { + /// Tracks where a new symbol declaration is found. + /// Generated for: + /// - Variable Declarations + /// - Type Definitions + /// - Directive Definitions + /// - Operation Definitions + Declaration { range: TextRange }, + + /// Tracks where a symbol is referenced regardless of its declaration position. + /// Generated for: + /// - All reference identifiers + Reference { + range: TextRange, + declared_at: TextRange, + }, + + /// Tracks references that do no have any matching binding + /// Generated for: + /// - Unmatched reference identifiers + UnresolvedReference { range: TextRange }, + + /// Tracks variable references that do no have any matching binding + /// Generated for: + /// - Unmatched variable reference identifiers + UnresolvedVariableReference { + range: TextRange, + referenced_operation: Option, + }, +} + +impl SemanticEvent { + pub fn range(&self) -> TextRange { + match self { + Self::Declaration { range, .. } + | Self::Reference { range, .. } + | Self::UnresolvedReference { range, .. } + | Self::UnresolvedVariableReference { range, .. } => *range, + } + } +} + +/// Extracts [SemanticEvent] from [GraphqlSyntaxNode]. +/// +/// The extraction is not entirely pull based, nor entirely push based. +/// This happens because some nodes can generate multiple events. +/// +/// For a simpler way to extract [SemanticEvent] see [semantic_events]. +/// +/// To use the [SemanticEventExtractor] one must push the current node, following +/// the pre-order of the tree, and must pull events until `pop` returns [None]. +/// +/// ```rust +/// use biome_graphql_parser::*; +/// use biome_graphql_syntax::*; +/// use biome_graphql_semantic::*; +/// let tree = parse_graphql("query { hero }"); +/// let mut extractor = SemanticEventExtractor::new(); +/// for e in tree.syntax().preorder() { +/// match e { +/// WalkEvent::Enter(node) => extractor.enter(&node), +/// WalkEvent::Leave(node) => extractor.leave(&node), +/// _ => {} +/// } +/// +/// while let Some(e) = extractor.pop() { +/// dbg!(e); +/// } +/// } +/// ``` +#[derive(Default, Debug)] +pub struct SemanticEventExtractor { + /// Event queue + stash: VecDeque, + /// Every available bindings and their range + bindings: FxHashMap, + + scopes: Vec, + /// Every available bindings and their range + references: FxHashMap>, + + current_scope: Option, +} + +/// A scope created by an operation or a fragment. +#[derive(Debug)] +struct Scope { + scope_id: usize, + range: TextRange, + /// Every references in this scope, used to track referenced fragments + references: FxHashMap>, + /// Every implicit variables references in this scope, which might be referenced directly in + /// this scope, or by any referenced fragments + implicit_variables_references: FxHashMap>, + /// Operation definition has variables definitions + variables_definitions: Option>, +} + +/// A binding name is either a type or a value. +/// +/// A value refer can refer to a directive or an operation. +/// A type can refer to a type definition like a scalar or an object. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +enum BindingName { + Type(TokenText), + Value(TokenText), +} + +#[derive(Debug, Clone)] +struct BindingInfo { + /// range of the name + range: TextRange, + /// If this is a variable binding, it will be defined in an operation scope + scope_id: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct ReferenceInfo { + /// range of the name + range: TextRange, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +struct VariableReferenceInfo { + range: TextRange, + name: TokenText, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +struct VariableBindingInfo { + range: TextRange, +} + +impl SemanticEventExtractor { + pub fn new() -> Self { + Self::default() + } + + /// See [SemanticEvent] for a more detailed description of which events [GraphqlSyntaxNode] generates. + #[inline] + pub fn enter(&mut self, node: &GraphqlSyntaxNode) { + match node.kind() { + GRAPHQL_NAME_BINDING => { + self.enter_identifier_binding(&GraphqlNameBinding::unwrap_cast(node.clone())); + } + + GRAPHQL_NAME_REFERENCE => { + self.enter_identifier_usage(&GraphqlNameReference::unwrap_cast(node.clone())); + } + + GRAPHQL_VARIABLE_BINDING => { + self.enter_variable_binding(&GraphqlVariableBinding::unwrap_cast(node.clone())); + } + + GRAPHQL_VARIABLE_REFERENCE => { + self.enter_variable_usage(&GraphqlVariableReference::unwrap_cast(node.clone())); + } + + GRAPHQL_OPERATION_DEFINITION => { + self.push_operation_scope(&GraphqlOperationDefinition::unwrap_cast(node.clone())); + } + + GRAPHQL_FRAGMENT_DEFINITION => { + self.push_fragment_scope(&GraphqlFragmentDefinition::unwrap_cast(node.clone())); + } + + _ => {} + } + } + + /// See [SemanticEvent] for a more detailed description + /// of which ```SyntaxNode``` generates which events. + #[inline] + pub fn leave(&mut self, node: &GraphqlSyntaxNode) { + match node.kind() { + GRAPHQL_ROOT => { + self.resolve_references(); + + self.resolve_variables_references(); + } + GRAPHQL_OPERATION_DEFINITION | GRAPHQL_FRAGMENT_DEFINITION => { + self.leave_scope(); + } + _ => {} + } + } + + /// Return any previous extracted [SemanticEvent]. + #[inline] + pub fn pop(&mut self) -> Option { + self.stash.pop_front() + } + + fn enter_identifier_binding(&mut self, node: &GraphqlNameBinding) { + let Ok(name_token) = node.value_token() else { + return; + }; + let name = name_token.token_text_trimmed(); + + let range = node.syntax().text_range(); + let Some(parent) = node.syntax().parent() else { + // every node aside from the root should have a parent, so this should never happen + return; + }; + self.stash.push_back(SemanticEvent::Declaration { range }); + if AnyGraphqlTypeDefinition::can_cast(parent.kind()) { + self.push_binding( + BindingName::Type(name), + BindingInfo { + range, + scope_id: self.get_current_scope_id(), + }, + ); + } else { + self.push_binding( + BindingName::Value(name), + BindingInfo { + range, + scope_id: self.get_current_scope_id(), + }, + ); + } + } + + fn enter_identifier_usage(&mut self, node: &GraphqlNameReference) { + let Ok(name_token) = node.value_token() else { + return; + }; + let name = name_token.token_text_trimmed(); + let range = node.syntax().text_range(); + let Some(parent) = node.syntax().parent() else { + // every node aside from the root should have a parent, so this should never happen + return; + }; + let binding_info = ReferenceInfo { range }; + let binding_name = match parent.kind() { + GRAPHQL_FIELD_DEFINITION + | GRAPHQL_IMPLEMENTS_INTERFACE_LIST + | GRAPHQL_INPUT_VALUE_DEFINITION + | GRAPHQL_LIST_TYPE + | GRAPHQL_ROOT_OPERATION_TYPE_DEFINITION + | GRAPHQL_TYPE_CONDITION + | GRAPHQL_UNION_MEMBER_TYPE_LIST + | GRAPHQL_VARIABLE_DEFINITION => BindingName::Type(name), + _ if AnyGraphqlTypeExtension::can_cast(parent.kind()) => BindingName::Type(name), + _ => BindingName::Value(name), + }; + self.push_reference(binding_name.clone(), binding_info.clone()); + if let Some(scope) = &mut self.current_scope { + scope + .references + .entry(binding_name) + .or_default() + .push(binding_info); + } + } + + fn enter_variable_binding(&mut self, node: &GraphqlVariableBinding) { + let Some(scope) = &mut self.current_scope else { + return; + }; + // We should be inside an operation scope + let Some(variables_definitions) = &mut scope.variables_definitions else { + return; + }; + let Ok(name) = node.name() else { + return; + }; + let Ok(name_token) = name.value_token() else { + return; + }; + let name_token = name_token.token_text_trimmed(); + let range = node.range(); + variables_definitions.insert(name_token, VariableBindingInfo { range }); + self.stash.push_back(SemanticEvent::Declaration { range }); + } + + fn enter_variable_usage(&mut self, node: &GraphqlVariableReference) { + let Some(scope) = &mut self.current_scope else { + return; + }; + let Ok(name_token) = node.name() else { + return; + }; + + let Ok(name_token) = name_token.value_token() else { + return; + }; + let name_token = name_token.token_text_trimmed(); + let range = node.syntax().text_range(); + scope + .implicit_variables_references + .entry(name_token.clone()) + .or_default() + .insert(VariableReferenceInfo { + range, + name: name_token, + }); + } + + fn push_operation_scope(&mut self, node: &GraphqlOperationDefinition) { + let range = node.syntax().text_range(); + self.current_scope = Some(Scope { + scope_id: self.scopes.len(), + range, + references: Default::default(), + implicit_variables_references: Default::default(), + variables_definitions: Some(Default::default()), + }); + } + + fn push_fragment_scope(&mut self, node: &GraphqlFragmentDefinition) { + let range = node.syntax().text_range(); + self.current_scope = Some(Scope { + scope_id: self.scopes.len(), + range, + references: Default::default(), + implicit_variables_references: Default::default(), + variables_definitions: None, + }); + } + + fn leave_scope(&mut self) { + if let Some(scope) = self.current_scope.take() { + self.scopes.push(scope); + } + } + + fn push_binding(&mut self, name: BindingName, info: BindingInfo) { + self.bindings.insert(name, info); + } + + fn push_reference(&mut self, name: BindingName, info: ReferenceInfo) { + self.references.entry(name).or_default().push(info); + } + + fn resolve_references(&mut self) { + for (name, references) in self.references.clone() { + if let Some(&BindingInfo { + range: declared_at, .. + }) = self.bindings.get(&name) + { + // We know the declaration of these reference. + for reference in references { + let event = SemanticEvent::Reference { + range: reference.range, + declared_at, + }; + self.stash.push_back(event); + } + } else { + for reference in references { + self.stash.push_back(SemanticEvent::UnresolvedReference { + range: reference.range, + }); + } + } + } + } + + fn resolve_variables_references(&mut self) { + let mut processed_scopes = HashSet::new(); + for scope_id in 0..self.scopes.len() { + self.resolve_scope_implicit_variables_references(scope_id, &mut processed_scopes); + } + + // Track processed variables to avoid emitting duplicate events + let mut processed_variables = HashSet::new(); + + // Bind variables to its declaration in operation definitions + for scope in &self.scopes { + // We are only interested in operation scopes + let Some(variables_definitions) = &scope.variables_definitions else { + continue; + }; + + for (name, variables) in &scope.implicit_variables_references { + if let Some(&VariableBindingInfo { range: declared_at }) = + variables_definitions.get(name) + { + // Bind those variables to its declaration + for variable in variables { + self.stash.push_back(SemanticEvent::Reference { + range: variable.range, + declared_at, + }); + processed_variables.insert(variable.range); + } + } else { + for variable in variables { + self.stash + .push_back(SemanticEvent::UnresolvedVariableReference { + range: variable.range, + referenced_operation: Some(scope.range), + }); + processed_variables.insert(variable.range); + } + } + } + } + + // Resolve remaining undefined variables references in fragments + for scope in &self.scopes { + let None = &scope.variables_definitions else { + continue; + }; + + for references in scope.implicit_variables_references.values() { + for reference in references { + if processed_variables.contains(&reference.range) { + continue; + } + self.stash + .push_back(SemanticEvent::UnresolvedVariableReference { + range: reference.range, + referenced_operation: None, + }); + } + } + } + } + + /// Recursively resolve implicit variables references from referenced fragments + fn resolve_scope_implicit_variables_references( + &mut self, + current_scope_id: usize, + processed_scopes: &mut HashSet, + ) { + // Prevent cycles + if processed_scopes.contains(¤t_scope_id) { + return; + } + processed_scopes.insert(current_scope_id); + let references = self.scopes[current_scope_id].references.clone(); + for (name, _) in references { + let Some(&BindingInfo { + scope_id: Some(binding_scope_id), + .. + }) = self.bindings.get(&name) + else { + continue; + }; + + // Only interested in fragment scopes + if self.scopes[binding_scope_id] + .variables_definitions + .is_some() + { + continue; + } + + // Bring every implicit variable reference from fragment definition's scope to the current scope + // Cycle detection is handled by a lint rule + self.resolve_scope_implicit_variables_references(binding_scope_id, processed_scopes); + + let binding_scope_variables = self.scopes[binding_scope_id] + .implicit_variables_references + .clone(); + + for (name, variables) in binding_scope_variables { + self.scopes[current_scope_id] + .implicit_variables_references + .entry(name) + .or_default() + .extend(variables); + } + } + } + + fn get_current_scope_id(&self) -> Option { + self.current_scope.as_ref().map(|s| s.scope_id) + } +} diff --git a/crates/biome_graphql_semantic/src/lib.rs b/crates/biome_graphql_semantic/src/lib.rs new file mode 100644 index 000000000000..704e9aab7f1d --- /dev/null +++ b/crates/biome_graphql_semantic/src/lib.rs @@ -0,0 +1,8 @@ +mod events; +mod semantic_model; + +pub use events::*; +pub use semantic_model::*; + +#[cfg(test)] +mod tests; diff --git a/crates/biome_graphql_semantic/src/semantic_model/binding.rs b/crates/biome_graphql_semantic/src/semantic_model/binding.rs new file mode 100644 index 000000000000..9554d2b7e310 --- /dev/null +++ b/crates/biome_graphql_semantic/src/semantic_model/binding.rs @@ -0,0 +1,111 @@ +use std::rc::Rc; + +use biome_graphql_syntax::{ + GraphqlDirective, GraphqlDirectiveDefinition, GraphqlFragmentDefinition, GraphqlFragmentSpread, + GraphqlNameBinding, GraphqlNameReference, GraphqlSyntaxNode, +}; +use biome_rowan::{AstNode, SyntaxNodeCast, TextRange}; + +use crate::SemanticModel; + +use super::{ + model::{SemanticIndex, SemanticModelData}, + reference::Reference, +}; + +/// Internal type with all the semantic data of a specific binding +#[derive(Debug)] +pub(crate) struct SemanticModelBinding { + pub index: SemanticIndex, + pub range: TextRange, +} + +/// Provides access to all semantic data of a specific binding. +pub struct Binding { + pub(crate) data: Rc, + pub(crate) index: SemanticIndex, +} + +impl std::fmt::Debug for Binding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Binding").field("id", &self.index).finish() + } +} + +impl Binding { + /// Returns the syntax node associated with this binding. + pub fn syntax(&self) -> &GraphqlSyntaxNode { + let binding = &self.data.bindings[self.index.0]; + &self.data.node_by_range[&binding.range] + } + + /// Returns the typed AST node associated with this binding. + pub fn tree(&self) -> GraphqlNameBinding { + let node = self.syntax(); + let binding = GraphqlNameBinding::cast_ref(node); + debug_assert!(binding.is_some()); + binding.unwrap() + } + + /// Returns an iterator to all references of this binding. + pub fn all_references(&self) -> Vec { + self.data.bindings_to_references[self.index.0] + .iter() + .map(|&x| Reference { + data: self.data.clone(), + index: x.into(), + }) + .collect::>() + } +} + +pub trait ReferenceExtensions { + fn all_references(&self, model: &SemanticModel) -> Vec; +} + +impl ReferenceExtensions for GraphqlNameBinding { + fn all_references(&self, model: &SemanticModel) -> Vec { + model.as_binding(self).all_references() + } +} + +pub trait IsBindingAstNode { + type ReferenceAstNode; + fn all_reference_nodes(&self, model: &SemanticModel) -> Vec; +} + +impl IsBindingAstNode for GraphqlNameBinding { + type ReferenceAstNode = GraphqlNameReference; + fn all_reference_nodes(&self, model: &SemanticModel) -> Vec { + self.all_references(model) + .iter() + .map(|r| r.tree()) + .collect() + } +} + +impl IsBindingAstNode for GraphqlDirectiveDefinition { + type ReferenceAstNode = GraphqlDirective; + fn all_reference_nodes(&self, model: &SemanticModel) -> Vec { + let Ok(name) = self.name() else { + return vec![]; + }; + name.all_reference_nodes(model) + .into_iter() + .filter_map(|r| r.syntax().parent()?.cast()) + .collect() + } +} + +impl IsBindingAstNode for GraphqlFragmentDefinition { + type ReferenceAstNode = GraphqlFragmentSpread; + fn all_reference_nodes(&self, model: &SemanticModel) -> Vec { + let Ok(name) = self.name() else { + return vec![]; + }; + name.all_reference_nodes(model) + .into_iter() + .filter_map(|r| r.syntax().parent()?.cast()) + .collect() + } +} diff --git a/crates/biome_graphql_semantic/src/semantic_model/builder.rs b/crates/biome_graphql_semantic/src/semantic_model/builder.rs new file mode 100644 index 000000000000..5b43bac165c8 --- /dev/null +++ b/crates/biome_graphql_semantic/src/semantic_model/builder.rs @@ -0,0 +1,151 @@ +use biome_graphql_syntax::{GraphqlRoot, GraphqlSyntaxKind, GraphqlSyntaxNode}; +use biome_rowan::{TextRange, TextSize}; +use rustc_hash::FxHashMap; + +use crate::{SemanticEvent, SemanticModelReference, SemanticModelUnresolvedVariableReference}; + +use super::{ + binding::SemanticModelBinding, + model::{SemanticModel, SemanticModelData}, + reference::SemanticModelUnresolvedReference, +}; + +/// Builds the [SemanticModel] consuming [SemanticEvent] and [GraphqlSyntaxNode]. +/// For a good example on how to use it see [semantic_model]. +/// +/// [SemanticModelBuilder] consumes all the [SemanticEvent] and build all the +/// data necessary to build a semantic model, that is allocated with an +/// [std::rc::Rc] and stored inside the [SemanticModel]. +pub struct SemanticModelBuilder { + root: GraphqlRoot, + node_by_range: FxHashMap, + bindings: Vec, + /// maps a binding range start to its index inside SemanticModelBuilder::bindings vec + bindings_by_start: FxHashMap, + // Map from each binding to its references + bindings_to_references: Vec>, + // List of all the references + references: Vec, + /// maps a reference range start to its index inside SemanticModelBuilder::references vec + references_by_start: FxHashMap, + // Map from each reference to its binding + references_to_bindings: Vec>, + unresolved_references: Vec, + unresolved_variable_references: Vec, +} + +impl SemanticModelBuilder { + pub fn new(root: GraphqlRoot) -> Self { + Self { + root, + node_by_range: FxHashMap::default(), + bindings: Vec::new(), + bindings_by_start: FxHashMap::default(), + bindings_to_references: Vec::new(), + references: Vec::new(), + references_by_start: FxHashMap::default(), + references_to_bindings: Vec::new(), + unresolved_references: Vec::new(), + unresolved_variable_references: Vec::new(), + } + } + + #[inline] + pub fn push_node(&mut self, node: &GraphqlSyntaxNode) { + use GraphqlSyntaxKind::*; + if matches!( + node.kind(), + GRAPHQL_NAME_BINDING + | GRAPHQL_NAME_REFERENCE + | GRAPHQL_VARIABLE_BINDING + | GRAPHQL_VARIABLE_REFERENCE + | GRAPHQL_OPERATION_DEFINITION + ) { + self.node_by_range.insert(node.text_range(), node.clone()); + } + } + + #[inline] + pub fn push_event(&mut self, e: SemanticEvent) { + use std::collections::hash_map::Entry; + use SemanticEvent::*; + match e { + Declaration { range } => { + let binding_id = self.bindings.len(); + self.bindings.push(SemanticModelBinding { + index: binding_id.into(), + range, + }); + self.bindings_by_start.insert(range.start(), binding_id); + self.bindings_to_references.push(Vec::new()); + } + Reference { + range, + declared_at: declaration_at, + } => { + let binding_id = self.bindings_by_start[&declaration_at.start()]; + let reference_id = + if let Entry::Vacant(e) = self.references_by_start.entry(range.start()) { + let reference_id = self.references.len(); + + self.references.push(SemanticModelReference { + index: reference_id.into(), + range, + }); + + e.insert(reference_id); + self.references_to_bindings.push(Vec::new()); + reference_id + } else { + self.references_by_start[&range.start()] + }; + self.references_to_bindings[reference_id].push(binding_id); + self.bindings_to_references[binding_id].push(reference_id); + } + + UnresolvedReference { range } => { + let node = &self.node_by_range[&range]; + let name = node.text_trimmed().to_string(); + if !Self::is_builtin_type(&name) && !Self::is_builtin_directive(&name) { + self.unresolved_references + .push(SemanticModelUnresolvedReference { range }) + } + } + UnresolvedVariableReference { + range, + referenced_operation, + } => { + self.unresolved_variable_references + .push(SemanticModelUnresolvedVariableReference { + range, + referenced_operation, + }) + } + } + } + + #[inline] + pub fn build(self) -> SemanticModel { + let data = SemanticModelData { + root: self.root, + node_by_range: self.node_by_range, + bindings: self.bindings, + bindings_by_start: self.bindings_by_start, + bindings_to_references: self.bindings_to_references, + references: self.references, + references_by_start: self.references_by_start, + references_to_bindings: self.references_to_bindings, + unresolved_references: self.unresolved_references, + unresolved_variable_references: self.unresolved_variable_references, + }; + SemanticModel::new(data) + } + + fn is_builtin_type(name: &str) -> bool { + matches!(name, "String" | "Int" | "Float" | "Boolean" | "ID") + } + + fn is_builtin_directive(name: &str) -> bool { + matches!(name, "skip" | "include" | "deprecated" | "specifiedBy") + } +} diff --git a/crates/biome_graphql_semantic/src/semantic_model/mod.rs b/crates/biome_graphql_semantic/src/semantic_model/mod.rs new file mode 100644 index 000000000000..f4a19df50206 --- /dev/null +++ b/crates/biome_graphql_semantic/src/semantic_model/mod.rs @@ -0,0 +1,38 @@ +mod binding; +mod builder; +mod model; +mod reference; + +use biome_graphql_syntax::GraphqlRoot; +use biome_rowan::AstNode; + +pub use binding::*; +pub use builder::*; +pub use model::*; +pub use reference::*; + +use crate::SemanticEventExtractor; + +/// Build the complete [SemanticModel] of a parsed file. +/// For a push based model to build the [SemanticModel], see [SemanticModelBuilder]. +pub fn semantic_model(root: &GraphqlRoot) -> SemanticModel { + let mut extractor = SemanticEventExtractor::default(); + let mut builder = SemanticModelBuilder::new(root.clone()); + + let root = root.syntax(); + for node in root.preorder() { + match node { + biome_graphql_syntax::WalkEvent::Enter(node) => { + builder.push_node(&node); + extractor.enter(&node); + } + biome_graphql_syntax::WalkEvent::Leave(node) => extractor.leave(&node), + } + } + + while let Some(e) = extractor.pop() { + builder.push_event(e); + } + + builder.build() +} diff --git a/crates/biome_graphql_semantic/src/semantic_model/model.rs b/crates/biome_graphql_semantic/src/semantic_model/model.rs new file mode 100644 index 000000000000..a6a1c580e8b6 --- /dev/null +++ b/crates/biome_graphql_semantic/src/semantic_model/model.rs @@ -0,0 +1,199 @@ +use std::rc::Rc; + +use biome_graphql_syntax::{ + GraphqlNameBinding, GraphqlNameReference, GraphqlRoot, GraphqlSyntaxNode, + GraphqlVariableReference, +}; +use biome_rowan::{AstNode, TextRange, TextSize}; +use rustc_hash::FxHashMap; + +use crate::{ + semantic_model::reference::UnresolvedReference, Reference, SemanticModelReference, + SemanticModelUnresolvedVariableReference, UnresolvedVariableReference, +}; + +use super::{ + binding::{Binding, SemanticModelBinding}, + reference::SemanticModelUnresolvedReference, +}; + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct SemanticIndex(pub(crate) usize); + +impl From for SemanticIndex { + fn from(v: usize) -> Self { + SemanticIndex(v) + } +} + +/// Contains all the data of the [SemanticModel] and only lives behind an [Rc]. +/// +/// That allows any returned struct (like [Scope], [Binding]) +/// to outlive the [SemanticModel], and to not include lifetimes. +#[derive(Debug)] +pub(crate) struct SemanticModelData { + pub(crate) root: GraphqlRoot, + // Map to each by its range + pub(crate) node_by_range: FxHashMap, + // List of all the declarations + pub(crate) bindings: Vec, + // Index bindings by range start + pub(crate) bindings_by_start: FxHashMap, + // Map from each binding to its references + pub(crate) bindings_to_references: Vec>, + // List of all the references + pub(crate) references: Vec, + // Map from each reference to its binding + pub(crate) references_to_bindings: Vec>, + // Index references by range start + pub(crate) references_by_start: FxHashMap, + /// All references that could not be resolved + pub(crate) unresolved_references: Vec, + /// All variable references that could not be resolved + pub(crate) unresolved_variable_references: Vec, +} + +impl PartialEq for SemanticModelData { + fn eq(&self, other: &Self) -> bool { + self.root == other.root + } +} + +impl Eq for SemanticModelData {} + +/// The façade for all semantic information. +/// - Bindings +/// - References +/// +/// See `SemanticModelData` for more information about the internals. +#[derive(Clone, Debug)] +pub struct SemanticModel { + pub(crate) data: Rc, +} + +impl SemanticModel { + pub(crate) fn new(data: SemanticModelData) -> Self { + Self { + data: Rc::new(data), + } + } + + pub fn all_bindings(&self) -> impl Iterator + '_ { + self.data.bindings.iter().map(|x| Binding { + data: self.data.clone(), + index: x.index, + }) + } + + pub fn all_references(&self) -> impl Iterator + '_ { + self.data.references.iter().map(|x| Reference { + data: self.data.clone(), + index: x.index, + }) + } + + /// Returns the [Binding] of a reference. + /// Can also be called from "binding" extension method. + /// + /// ```rust + /// use biome_graphql_parser::parse_graphql; + /// use biome_rowan::{AstNode, SyntaxNodeCast}; + /// use biome_graphql_syntax::GraphqlNameReference; + /// use biome_graphql_semantic::{semantic_model, BindingExtensions}; + /// + /// let r = parse_graphql("{ ...fragment }"); + /// let model = semantic_model(&r.tree()); + /// + /// let fragment_reference = r + /// .syntax() + /// .descendants() + /// .filter_map(|x| x.cast::()) + /// .find(|x| x.text() == "fragment") + /// .unwrap(); + /// + /// let fragment_binding = model.binding(&fragment_reference); + /// // or + /// let fragment_binding = fragment_reference.binding(&model); + /// ``` + pub fn binding(&self, reference: &GraphqlNameReference) -> Option { + let range = reference.syntax().text_range(); + let reference_id = self.data.references_by_start.get(&range.start())?; + debug_assert!(self.data.references_to_bindings[*reference_id].len() <= 1); + self.data.references_to_bindings[*reference_id] + .iter() + .map(|&x| Binding { + data: self.data.clone(), + index: x.into(), + }) + .next() + } + + /// Returns the [Binding] of a variable reference. + /// Since a variable can be referenced in a fragment, which in turn can be referenced + /// by multiple operations, and then defined in those operations, this method returns + /// a list of bindings. + /// + /// ```rust + /// use biome_graphql_parser::parse_graphql; + /// use biome_rowan::{AstNode, SyntaxNodeCast}; + /// use biome_graphql_syntax::GraphqlVariableReference; + /// use biome_graphql_semantic::semantic_model; + /// + /// let r = parse_graphql("{ field(arg: $var) }"); + /// let model = semantic_model(&r.tree()); + /// + /// let fragment_reference = r + /// .syntax() + /// .descendants() + /// .filter_map(|x| x.cast::()) + /// .find(|x| x.text() == "$var") + /// .unwrap(); + /// + /// let fragment_bindings = model.bindings(&fragment_reference); + /// ``` + pub fn bindings(&self, reference: &GraphqlVariableReference) -> Vec { + let range = reference.syntax().text_range(); + let Some(reference_id) = self.data.references_by_start.get(&range.start()) else { + return Vec::new(); + }; + self.data.references_to_bindings[*reference_id] + .iter() + .map(|&x| Binding { + data: self.data.clone(), + index: x.into(), + }) + .collect::>() + } + + /// Returns an iterator of all the unresolved references in the program + pub fn all_unresolved_references(&self) -> impl Iterator + '_ { + (0..self.data.unresolved_references.len()).map(move |id| UnresolvedReference { + data: self.data.clone(), + id, + }) + } + + /// Returns an iterator of all the unresolved variable references in the program + pub fn all_unresolved_variable_references( + &self, + ) -> impl Iterator + '_ { + self.data + .unresolved_variable_references + .iter() + .enumerate() + .map(move |(id, reference)| UnresolvedVariableReference { + data: self.data.clone(), + referenced_operation: reference.referenced_operation, + id, + }) + } + + pub fn as_binding(&self, binding: &GraphqlNameBinding) -> Binding { + let range = binding.syntax().text_range(); + let id = self.data.bindings_by_start[&range.start()]; + Binding { + data: self.data.clone(), + index: id.into(), + } + } +} diff --git a/crates/biome_graphql_semantic/src/semantic_model/reference.rs b/crates/biome_graphql_semantic/src/semantic_model/reference.rs new file mode 100644 index 000000000000..659c6b2c3ffe --- /dev/null +++ b/crates/biome_graphql_semantic/src/semantic_model/reference.rs @@ -0,0 +1,246 @@ +use std::rc::Rc; + +use biome_graphql_syntax::{ + AnyGraphqlTypeDefinition, GraphqlDirective, GraphqlDirectiveDefinition, + GraphqlEnumTypeDefinition, GraphqlEnumTypeExtension, GraphqlFragmentDefinition, + GraphqlFragmentSpread, GraphqlInterfaceTypeDefinition, GraphqlInterfaceTypeExtension, + GraphqlNameBinding, GraphqlObjectTypeDefinition, GraphqlObjectTypeExtension, + GraphqlOperationDefinition, GraphqlScalarTypeDefinition, GraphqlScalarTypeExtension, + GraphqlTypeCondition, GraphqlUnionTypeDefinition, GraphqlUnionTypeExtension, + GraphqlVariableBinding, GraphqlVariableReference, +}; +use biome_graphql_syntax::{GraphqlNameReference, GraphqlSyntaxNode}; +use biome_rowan::TextRange; +use biome_rowan::{AstNode, SyntaxNodeCast}; + +use crate::{SemanticIndex, SemanticModel}; + +use super::{binding::Binding, model::SemanticModelData}; + +/// Internal type with all the semantic data of a specific reference +#[derive(Debug)] +pub(crate) struct SemanticModelReference { + pub(crate) index: SemanticIndex, + pub(crate) range: TextRange, +} + +/// Provides all information regarding to a specific reference. +#[derive(Debug)] +pub struct Reference { + pub(crate) data: Rc, + pub(crate) index: SemanticIndex, +} + +impl Reference { + /// Returns the range of this reference + pub fn range(&self) -> &TextRange { + let reference = &self.data.references[self.index.0]; + &reference.range + } + + /// Returns the node of this reference + pub fn syntax(&self) -> &GraphqlSyntaxNode { + &self.data.node_by_range[self.range()] + } + + pub fn tree(&self) -> GraphqlNameReference { + let node = self.syntax(); + + let reference = GraphqlNameReference::cast_ref(node); + debug_assert!(reference.is_some()); + reference.unwrap() + } + + /// Returns the binding of this reference + pub fn all_bindings(&self) -> Vec { + self.data.references_to_bindings[self.index.0] + .iter() + .map(|&x| Binding { + data: self.data.clone(), + index: x.into(), + }) + .collect::>() + } +} + +#[derive(Debug)] +pub struct SemanticModelUnresolvedReference { + pub(crate) range: TextRange, +} + +#[derive(Debug)] +pub struct UnresolvedReference { + pub(crate) data: Rc, + pub(crate) id: usize, +} + +impl UnresolvedReference { + pub fn syntax(&self) -> &GraphqlSyntaxNode { + let reference = &self.data.unresolved_references[self.id]; + &self.data.node_by_range[&reference.range] + } + + pub fn tree(&self) -> GraphqlNameReference { + GraphqlNameReference::unwrap_cast(self.syntax().clone()) + } + + pub fn range(&self) -> &TextRange { + let reference = &self.data.unresolved_references[self.id]; + &reference.range + } +} + +#[derive(Debug)] +pub struct SemanticModelUnresolvedVariableReference { + pub(crate) range: TextRange, + pub(crate) referenced_operation: Option, +} + +#[derive(Debug)] +pub struct UnresolvedVariableReference { + pub(crate) data: Rc, + pub(crate) id: usize, + pub(crate) referenced_operation: Option, +} + +impl UnresolvedVariableReference { + pub fn syntax(&self) -> &GraphqlSyntaxNode { + let reference = &self.data.unresolved_variable_references[self.id]; + &self.data.node_by_range[&reference.range] + } + + pub fn tree(&self) -> GraphqlVariableReference { + GraphqlVariableReference::unwrap_cast(self.syntax().clone()) + } + + pub fn range(&self) -> &TextRange { + let reference = &self.data.unresolved_variable_references[self.id]; + &reference.range + } + + pub fn referenced_operation(&self) -> Option { + self.referenced_operation + .as_ref() + .and_then(|range| self.data.node_by_range[range].clone().cast()) + } +} + +pub trait BindingExtensions { + fn binding(&self, model: &SemanticModel) -> Option; +} + +impl BindingExtensions for GraphqlNameReference { + fn binding(&self, model: &SemanticModel) -> Option { + model.binding(self) + } +} + +pub trait HasDeclarationAstNode { + type DeclarationAstNode; + fn binding_node(&self, model: &SemanticModel) -> Option; +} + +impl HasDeclarationAstNode for GraphqlNameReference { + type DeclarationAstNode = GraphqlNameBinding; + fn binding_node(&self, model: &SemanticModel) -> Option { + let binding = model.binding(self)?; + let name_binding = binding.syntax().clone().cast()?; + Some(name_binding) + } +} + +impl HasDeclarationAstNode for GraphqlFragmentSpread { + type DeclarationAstNode = GraphqlFragmentDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let fragment_definition = name_binding.syntax().parent()?.cast()?; + Some(fragment_definition) + } +} + +impl HasDeclarationAstNode for GraphqlDirective { + type DeclarationAstNode = GraphqlDirectiveDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let directive_definition = name_binding.syntax().parent()?.cast()?; + Some(directive_definition) + } +} + +impl HasDeclarationAstNode for GraphqlTypeCondition { + type DeclarationAstNode = AnyGraphqlTypeDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let type_name = self.ty().ok()?; + let name_binding = type_name.binding_node(model)?; + let definition = name_binding.syntax().parent()?.cast()?; + Some(definition) + } +} + +impl HasDeclarationAstNode for GraphqlScalarTypeExtension { + type DeclarationAstNode = GraphqlScalarTypeDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let definition = name_binding.syntax().parent()?.cast()?; + Some(definition) + } +} + +impl HasDeclarationAstNode for GraphqlObjectTypeExtension { + type DeclarationAstNode = GraphqlObjectTypeDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let definition = name_binding.syntax().parent()?.cast()?; + Some(definition) + } +} + +impl HasDeclarationAstNode for GraphqlInterfaceTypeExtension { + type DeclarationAstNode = GraphqlInterfaceTypeDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let definition = name_binding.syntax().parent()?.cast()?; + Some(definition) + } +} + +impl HasDeclarationAstNode for GraphqlUnionTypeExtension { + type DeclarationAstNode = GraphqlUnionTypeDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let definition = name_binding.syntax().parent()?.cast()?; + Some(definition) + } +} + +impl HasDeclarationAstNode for GraphqlEnumTypeExtension { + type DeclarationAstNode = GraphqlEnumTypeDefinition; + fn binding_node(&self, model: &SemanticModel) -> Option { + let name = self.name().ok()?; + let name_binding = name.binding_node(model)?; + let definition = name_binding.syntax().parent()?.cast()?; + Some(definition) + } +} + +pub trait HasDeclarationAstNodes { + type DeclarationAstNode; + fn binding_nodes(&self, model: &SemanticModel) -> Vec; +} + +impl HasDeclarationAstNodes for GraphqlVariableReference { + type DeclarationAstNode = GraphqlVariableBinding; + fn binding_nodes(&self, model: &SemanticModel) -> Vec { + model + .bindings(self) + .iter() + .filter_map(|binding| binding.syntax().clone().cast()) + .collect() + } +} diff --git a/crates/biome_graphql_semantic/src/tests/directive.rs b/crates/biome_graphql_semantic/src/tests/directive.rs new file mode 100644 index 000000000000..6f5788f3fe2b --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/directive.rs @@ -0,0 +1,57 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlDirective; +use biome_graphql_syntax::GraphqlDirectiveDefinition; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; +use crate::IsBindingAstNode; + +use super::assert_nodes_eq; +use super::extract_node; + +#[test] +fn ok_directive_reference() { + let src = r#" +directive @example on FIELD + +{ + hero @example +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["example"]); + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let directive_definition = extract_node::(&parse_result); + let directive = extract_node::(&parse_result); + let directive_binding = directive.binding_node(&model).unwrap(); + + assert_eq!(directive_binding, directive_definition); + + let directive_reference = directive_definition.all_reference_nodes(&model); + assert_eq!(directive_reference, vec![directive]); +} + +#[test] +fn ok_builtin_directive() { + let src = r#" +type Query { + hero: String @deprecated(reason: "Use `hero` field instead") + skip: String @skip(if: true) + include: String @include(if: false) +} +scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") +"#; + + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Query", "UUID"]); + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); +} diff --git a/crates/biome_graphql_semantic/src/tests/enum.rs b/crates/biome_graphql_semantic/src/tests/enum.rs new file mode 100644 index 000000000000..301a6c82b838 --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/enum.rs @@ -0,0 +1,72 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlEnumTypeDefinition; +use biome_graphql_syntax::GraphqlEnumTypeExtension; +use biome_graphql_syntax::GraphqlFieldDefinition; +use biome_graphql_syntax::GraphqlNameReference; +use biome_rowan::AstNode; +use biome_rowan::SyntaxNodeCast; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; + +use super::assert_nodes_eq; +use super::extract_binding_definition; +use super::extract_node; + +#[test] +fn ok_enum_type() { + let src = r#" +enum Misc { + First +} + +type Query { + misc: Misc +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Misc", "Query"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let enum_definition = extract_node::(&parse_result); + let field_definition = extract_node::(&parse_result); + let enum_reference = field_definition + .ty() + .unwrap() + .syntax() + .clone() + .cast::() + .unwrap(); + let enum_reference = + extract_binding_definition::(&enum_reference, &model); + assert_eq!(enum_reference, enum_definition); +} + +#[test] +fn ok_enum_extension() { + let src = r#" +enum Misc = { + First +} +extend enum Misc @example +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Misc"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["example"]); + + let enum_definition = extract_node::(&parse_result); + + let enum_extension = extract_node::(&parse_result); + let enum_binding = enum_extension.binding_node(&model).unwrap(); + assert_eq!(enum_binding, enum_definition); +} diff --git a/crates/biome_graphql_semantic/src/tests/fragment.rs b/crates/biome_graphql_semantic/src/tests/fragment.rs new file mode 100644 index 000000000000..99ff3633e25a --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/fragment.rs @@ -0,0 +1,69 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlFragmentDefinition; +use biome_graphql_syntax::GraphqlFragmentSpread; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; +use crate::IsBindingAstNode; + +use super::assert_nodes_eq; +use super::extract_node; + +#[test] +fn ok_fragment_spread() { + let src = r#" +fragment HeroDetails on Character { + name +} + +query query { + hero { + ...HeroDetails + } +}"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["HeroDetails", "query"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["Character"]); + + let fragment_definition = extract_node::(&parse_result); + let fragment_spread = extract_node::(&parse_result); + let fragment_binding = fragment_spread.binding_node(&model).unwrap(); + + assert_eq!(fragment_binding, fragment_definition); + + let fragment_reference = fragment_definition.all_reference_nodes(&model); + assert_eq!(fragment_reference, vec![fragment_spread]); +} + +#[test] +fn undefined_variable_reference_in_fragment() { + let src = r#" +fragment HeroDetails on Character { + name(startWith: $start) +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["HeroDetails"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["Character"]); + + let unresolved_variable_references = model + .all_unresolved_variable_references() + .collect::>(); + assert_eq!(unresolved_variable_references.len(), 1); + let unresolved_variable_reference = &unresolved_variable_references[0]; + assert_eq!( + unresolved_variable_reference.syntax().text_trimmed(), + "$start" + ); + assert_eq!(unresolved_variable_reference.referenced_operation(), None); +} diff --git a/crates/biome_graphql_semantic/src/tests/interface.rs b/crates/biome_graphql_semantic/src/tests/interface.rs new file mode 100644 index 000000000000..a785cb7fb26b --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/interface.rs @@ -0,0 +1,125 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlImplementsInterfaces; +use biome_graphql_syntax::GraphqlInterfaceTypeDefinition; +use biome_graphql_syntax::GraphqlInterfaceTypeExtension; +use biome_graphql_syntax::GraphqlObjectTypeDefinition; +use biome_graphql_syntax::GraphqlTypeCondition; +use biome_rowan::AstNode; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; +use crate::SemanticModel; + +use super::assert_nodes_eq; +use super::extract_binding_definition; +use super::extract_node; +use super::extract_node_by_name; + +#[test] +fn ok_interface_extension() { + let src = r#" +interface Character { + name: String! +} + +extend interface Character @example"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Character"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["example"]); + + let character_type_definition = + extract_node_by_name::(&parse_result, "Character"); + + let character_type_extension = extract_node::(&parse_result); + let character_type_binding = character_type_extension.binding_node(&model).unwrap(); + assert_eq!(character_type_binding, character_type_definition); +} + +#[test] +fn ok_implements_interface() { + let src = r#" +interface Character { + name: String! +} + +type Hero implements Character { + name: String! +} + +interface AnotherCharacter implements Character { + name: String! +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Character", "Hero", "AnotherCharacter"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let character_type_definition = + extract_node_by_name::(&parse_result, "Character"); + let another_character_type_definition = + extract_node_by_name::(&parse_result, "AnotherCharacter"); + let object_type_definition = extract_node::(&parse_result); + let character_type_binding = extract_implemented_interfaces_definitions( + &object_type_definition.implements().unwrap(), + &model, + ); + + let character_type_definition = vec![character_type_definition.clone()]; + assert_eq!(character_type_binding, character_type_definition); + + let character_type_binding = extract_implemented_interfaces_definitions( + &another_character_type_definition.implements().unwrap(), + &model, + ); + assert_eq!(character_type_binding, character_type_definition); +} + +#[test] +fn ok_interface_type() { + let src = r#" +interface Character { + name: String! +} + +fragment HeroDetails on Character { + name +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Character", "HeroDetails"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let character_type_definition = + extract_node_by_name::(&parse_result, "Character"); + let type_condition = extract_node::(&parse_result); + let character_type_binding = type_condition.binding_node(&model).unwrap(); + let character_type_binding = + GraphqlInterfaceTypeDefinition::cast(character_type_binding.into()).unwrap(); + assert_eq!(character_type_binding, character_type_definition); +} + +fn extract_implemented_interfaces_definitions( + implements_interfaces: &GraphqlImplementsInterfaces, + model: &SemanticModel, +) -> Vec { + implements_interfaces + .interfaces() + .into_iter() + .map(|interface| extract_binding_definition(&interface.unwrap(), model)) + .collect() +} diff --git a/crates/biome_graphql_semantic/src/tests/mod.rs b/crates/biome_graphql_semantic/src/tests/mod.rs new file mode 100644 index 000000000000..ae81d9b0f9bb --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/mod.rs @@ -0,0 +1,100 @@ +mod directive; +mod r#enum; +mod fragment; +mod interface; +mod object; +mod operation; +mod scalar; +mod union; + +use biome_graphql_parser::GraphqlParse; +use biome_graphql_syntax::GraphqlLanguage; +use biome_graphql_syntax::GraphqlNameReference; +use biome_rowan::AstNode; +use biome_rowan::SyntaxNodeCast; + +use crate::Binding; +use crate::HasDeclarationAstNode; +use crate::SemanticModel; +use crate::UnresolvedReference; +use crate::UnresolvedVariableReference; + +fn extract_node(parse_result: &GraphqlParse) -> T +where + T: biome_rowan::AstNode, +{ + parse_result + .syntax() + .descendants() + .find_map(|node| node.cast::()) + .unwrap() +} + +/// Extracts the first node with a specific name. +/// Since most nodes' name are stored in a GraphqlName* child node, +/// we can first find the nodes by name then cast the parent to get the desired node. +fn extract_node_by_name(parse_result: &GraphqlParse, name: &str) -> T +where + T: biome_rowan::AstNode, +{ + parse_result + .syntax() + .descendants() + .filter(|node| node.text_trimmed() == name) + .find_map(|node| { + let parent = node.parent()?; + parent.cast::() + }) + .unwrap() +} + +fn extract_nodes(parse_result: &GraphqlParse) -> Vec +where + T: biome_rowan::AstNode, +{ + parse_result + .syntax() + .descendants() + .filter_map(|node| node.cast::()) + .collect() +} + +/// Extracts the binding definition from a reference. +/// Since most GraphqlNameReference nodes are used as a child of a definition node, +/// we can extract the parent of the reference then cast it to get the definition. +fn extract_binding_definition(node: &GraphqlNameReference, model: &SemanticModel) -> T +where + T: AstNode, +{ + let binding = node.binding_node(model).unwrap(); + binding.syntax().parent().unwrap().cast().unwrap() +} + +fn assert_nodes_eq(a: impl IntoIterator, b: &[&str]) { + assert_eq!( + a.into_iter().map(|x| x.to_text()).collect::>(), + b.iter().map(|x| (*x).to_string()).collect::>() + ); +} + +trait ToText { + fn to_text(self) -> String; +} + +impl ToText for Binding { + fn to_text(self) -> String { + self.syntax().text_trimmed().to_string() + } +} + +impl ToText for UnresolvedReference { + fn to_text(self) -> String { + self.syntax().text_trimmed().to_string() + } +} + +impl ToText for UnresolvedVariableReference { + fn to_text(self) -> String { + self.syntax().text_trimmed().to_string() + } +} diff --git a/crates/biome_graphql_semantic/src/tests/object.rs b/crates/biome_graphql_semantic/src/tests/object.rs new file mode 100644 index 000000000000..396464931a00 --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/object.rs @@ -0,0 +1,48 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlObjectTypeDefinition; +use biome_graphql_syntax::GraphqlObjectTypeExtension; +use biome_graphql_syntax::GraphqlTypeCondition; +use biome_rowan::AstNode; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; + +use super::assert_nodes_eq; +use super::extract_node; + +#[test] +fn ok_object_type() { + let src = r#" +type Character { + name: String! +} + +fragment HeroDetails on Character { + name +} + +schema { + query: Character +} + +extend type Character @example"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Character", "HeroDetails"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["example"]); + + let object_type_definition = extract_node::(&parse_result); + let type_condition = extract_node::(&parse_result); + let object_type_binding = type_condition.binding_node(&model).unwrap(); + let object_type_binding = + GraphqlObjectTypeDefinition::cast(object_type_binding.into()).unwrap(); + assert_eq!(object_type_binding, object_type_definition); + + let object_type_extension = extract_node::(&parse_result); + let object_type_binding = object_type_extension.binding_node(&model).unwrap(); + assert_eq!(object_type_binding, object_type_definition,); +} diff --git a/crates/biome_graphql_semantic/src/tests/operation.rs b/crates/biome_graphql_semantic/src/tests/operation.rs new file mode 100644 index 000000000000..e99ff03bb340 --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/operation.rs @@ -0,0 +1,189 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlArgument; +use biome_graphql_syntax::GraphqlOperationDefinition; +use biome_graphql_syntax::GraphqlVariableBinding; +use biome_graphql_syntax::GraphqlVariableDefinition; +use biome_graphql_syntax::GraphqlVariableReference; + +use crate::semantic_model; +use crate::tests::extract_nodes; +use crate::HasDeclarationAstNodes; + +use super::assert_nodes_eq; +use super::extract_node; + +#[test] +fn ok_variable() { + let src = r#" +query ($storyId: ID = "1") { + likeStory(storyId: $storyId) +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["$storyId"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let variable_definitions = extract_nodes::(&parse_result) + .into_iter() + .map(|x| x.variable().unwrap()) + .collect::>(); + let argument = extract_node::(&parse_result); + let variable_reference = argument.value().unwrap(); + let variable_reference = variable_reference.as_graphql_variable_reference().unwrap(); + let variable_binding = variable_reference.binding_nodes(&model); + assert_eq!(variable_binding, variable_definitions); +} + +#[test] +fn ok_variable_in_fragment() { + let src = r#" +type Story {} + +fragment StoryDetails on Story { + likeStory(storyId: $storyId) +} +fragment PostDetails on Story { + viewer(largerThan: $someNumber) +} +query ($storyId: ID = "1", $someNumber: Int = 10) { + ...StoryDetails + ...PostDetails +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq( + bindings, + &[ + "Story", + "StoryDetails", + "PostDetails", + "$storyId", + "$someNumber", + ], + ); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let expected_variable_bindings = extract_nodes::(&parse_result); + let variable_references = extract_nodes::(&parse_result); + let variable_bindings = variable_references + .into_iter() + .flat_map(|re| re.binding_nodes(&model)) + .collect::>(); + assert_eq!(variable_bindings, expected_variable_bindings); +} + +#[test] +fn ok_variable_in_nested_fragment() { + let src = r#" +type Story {} +fragment A on Story { + first(arg: $a) +} +fragment B on Story { + ...A, + second(arg: $b) +} +fragment C on Story { + third(arg: $c) +} +fragment D on Story { + ...C, + fourth(arg: $d) +} +query ($a: Int, $b: Int, $c: Int, $d: Int) { + ...B, + ...D +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq( + bindings, + &["Story", "A", "B", "C", "D", "$a", "$b", "$c", "$d"], + ); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let expected_variable_bindings = extract_nodes::(&parse_result); + let variable_references = extract_nodes::(&parse_result); + let variable_bindings = variable_references + .into_iter() + .flat_map(|re| re.binding_nodes(&model)) + .collect::>(); + assert_eq!(variable_bindings.len(), 4); + assert_eq!(variable_bindings, expected_variable_bindings); +} + +#[test] +fn ok_variable_in_multiple_operations() { + let src = r#" +type Story {} +fragment A on Story { + first(arg: $a) +} +query First($a: Int) { + ...A, +} +query Second($a: Int) { + ...A, +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Story", "A", "First", "$a", "Second", "$a"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let expected_variable_bindings = extract_nodes::(&parse_result); + let variable_reference = extract_node::(&parse_result); + let variable_bindings = variable_reference.binding_nodes(&model); + assert_eq!(variable_bindings, expected_variable_bindings); +} + +#[test] +fn undefined_variable_in_operations() { + let src = r#" +type Story {} +fragment A on Story { + first(arg: $a) +} +query First { + ...A, +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Story", "A", "First"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let unresolved_variable_references = model + .all_unresolved_variable_references() + .collect::>(); + assert_eq!(unresolved_variable_references.len(), 1); + let unresolved_variable_reference = &unresolved_variable_references[0]; + let referenced_operation = unresolved_variable_reference + .referenced_operation() + .unwrap(); + let expected_operation_definition = extract_node::(&parse_result); + assert_eq!(referenced_operation, expected_operation_definition); +} diff --git a/crates/biome_graphql_semantic/src/tests/scalar.rs b/crates/biome_graphql_semantic/src/tests/scalar.rs new file mode 100644 index 000000000000..c996a983f1f4 --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/scalar.rs @@ -0,0 +1,54 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlScalarTypeDefinition; +use biome_graphql_syntax::GraphqlScalarTypeExtension; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; + +use super::assert_nodes_eq; +use super::extract_node; +use super::extract_nodes; + +#[test] +fn ok_scalar_extension() { + let src = r#" +scalar Date + +extend scalar Date @example +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Date"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["example"]); + + let scalar_definitions = extract_nodes::(&parse_result); + + let scalar_extension = extract_node::(&parse_result); + let scalar_definition = scalar_extension.binding_node(&model).unwrap(); + assert_eq!(&scalar_definition, scalar_definitions.first().unwrap()); +} + +#[test] +fn ok_builtin_scalar() { + let src = r#" +type Query { + int: Int + float: Float + string: String + boolean: Boolean + id: ID +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Query"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); +} diff --git a/crates/biome_graphql_semantic/src/tests/union.rs b/crates/biome_graphql_semantic/src/tests/union.rs new file mode 100644 index 000000000000..df055c74d599 --- /dev/null +++ b/crates/biome_graphql_semantic/src/tests/union.rs @@ -0,0 +1,116 @@ +use biome_graphql_parser::parse_graphql; +use biome_graphql_syntax::GraphqlFieldDefinition; +use biome_graphql_syntax::GraphqlNameReference; +use biome_graphql_syntax::GraphqlObjectTypeDefinition; +use biome_graphql_syntax::GraphqlUnionTypeDefinition; +use biome_graphql_syntax::GraphqlUnionTypeExtension; +use biome_rowan::AstNode; +use biome_rowan::SyntaxNodeCast; + +use crate::semantic_model; +use crate::HasDeclarationAstNode; +use crate::SemanticModel; + +use super::assert_nodes_eq; +use super::extract_binding_definition; +use super::extract_node; +use super::extract_nodes; + +#[test] +fn ok_union_of_objects() { + let src = r#" +type First { + name: String! +} + +type Second { + name: String! +} + +type Third { + name: String! +} + +union Misc = First | Second | Third +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["First", "Second", "Third", "Misc"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &[]); + + let union_definition = extract_node::(&parse_result); + let object_type_definitions = extract_nodes::(&parse_result); + let united_type_definitions = extract_united_type_definitions(&union_definition, &model); + assert_eq!(united_type_definitions, object_type_definitions); +} + +#[test] +fn ok_union_type() { + let src = r#" +union Misc = First | Second + +type Query { + misc: Misc +} +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Misc", "Query"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["Second", "First"]); + + let union_definition = extract_node::(&parse_result); + let field_definition = extract_node::(&parse_result); + let union_reference = field_definition + .ty() + .unwrap() + .syntax() + .clone() + .cast::() + .unwrap(); + let union_reference = + extract_binding_definition::(&union_reference, &model); + assert_eq!(union_reference, union_definition); +} + +#[test] +fn ok_union_extension() { + let src = r#" +union Misc = First +extend union Misc @example +"#; + let parse_result = parse_graphql(src); + let model = semantic_model(&parse_result.tree()); + let bindings = model.all_bindings().collect::>(); + + assert_nodes_eq(bindings, &["Misc"]); + + let unresolved_references = model.all_unresolved_references().collect::>(); + assert_nodes_eq(unresolved_references, &["example", "First"]); + + let union_definition = extract_node::(&parse_result); + + let union_extension = extract_node::(&parse_result); + let union_binding = union_extension.binding_node(&model).unwrap(); + assert_eq!(union_binding, union_definition); +} + +fn extract_united_type_definitions( + union: &GraphqlUnionTypeDefinition, + model: &SemanticModel, +) -> Vec { + union + .union_members() + .unwrap() + .members() + .into_iter() + .map(|member| extract_binding_definition(&member.unwrap(), model)) + .collect() +} diff --git a/knope.toml b/knope.toml index b6660cc4447e..b95f730b33b9 100644 --- a/knope.toml +++ b/knope.toml @@ -204,6 +204,10 @@ versioned_files = ["crates/biome_graphql_formatter/Cargo.toml"] changelog = "crates/biome_graphql_analyze/CHANGELOG.md" versioned_files = ["crates/biome_graphql_analyze/Cargo.toml"] +[packages.biome_graphql_semantic] +changelog = "crates/biome_graphql_semantic/CHANGELOG.md" +versioned_files = ["crates/biome_graphql_semantic/Cargo.toml"] + ## End of crates. DO NOT CHANGE! # Workflow to create a changeset