diff --git a/Cargo.lock b/Cargo.lock index feba100a9da5..ba51dba88123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,6 +548,8 @@ dependencies = [ "grit-pattern-matcher", "grit-util", "im", + "path-absolutize", + "regex", "rustc-hash", "serde", ] @@ -2502,6 +2504,24 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "path-absolutize" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" +dependencies = [ + "once_cell", +] + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/crates/biome_grit_patterns/Cargo.toml b/crates/biome_grit_patterns/Cargo.toml index 50b1271f23d9..7f24ff1850d4 100644 --- a/crates/biome_grit_patterns/Cargo.toml +++ b/crates/biome_grit_patterns/Cargo.toml @@ -24,6 +24,8 @@ biome_rowan = { workspace = true } grit-pattern-matcher = { version = "0.3" } grit-util = { version = "0.3" } im = { version = "15.1.0" } +path-absolutize = { version = "3.1.1", optional = false, features = ["use_unix_paths_on_wasm"] } +regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/biome_grit_patterns/src/errors.rs b/crates/biome_grit_patterns/src/errors.rs index bf3221577e6d..416830d04ff5 100644 --- a/crates/biome_grit_patterns/src/errors.rs +++ b/crates/biome_grit_patterns/src/errors.rs @@ -1,6 +1,7 @@ use biome_diagnostics::serde::Diagnostic as SerializableDiagnostic; use biome_diagnostics::Diagnostic; use biome_rowan::SyntaxError; +use grit_util::ByteRange; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Diagnostic, Serialize)] @@ -37,6 +38,12 @@ pub enum CompileError { /// If a function or bubble pattern has multiple parameters with the same name. DuplicateParameters, + /// A metavariable was expected at the given range. + InvalidMetavariableRange(ByteRange), + + /// Incorrect reference to a metavariable. + MetavariableNotFound(String), + /// Tried to declare or assign a Grit reserved metavariable. ReservedMetavariable(String), diff --git a/crates/biome_grit_patterns/src/grit_binding.rs b/crates/biome_grit_patterns/src/grit_binding.rs index 3ae1d3fb36d9..951efcc34801 100644 --- a/crates/biome_grit_patterns/src/grit_binding.rs +++ b/crates/biome_grit_patterns/src/grit_binding.rs @@ -1,21 +1,32 @@ use crate::{ grit_context::GritQueryContext, grit_target_language::GritTargetLanguage, - grit_target_node::GritTargetNode, + grit_target_node::GritTargetNode, grit_tree::GritTree, source_location_ext::SourceFileExt, }; +use biome_diagnostics::{display::SourceFile, SourceCode}; use grit_pattern_matcher::{binding::Binding, constant::Constant}; -use grit_util::{ByteRange, CodeRange, Range}; +use grit_util::{Ast, AstNode, ByteRange, CodeRange, Range}; use std::path::Path; #[derive(Clone, Debug, PartialEq)] -pub(crate) struct GritBinding; +pub(crate) enum GritBinding<'a> { + Tree(&'a GritTree), + Node(GritTargetNode), + Constant(&'a Constant), +} -impl<'a> Binding<'a, GritQueryContext> for GritBinding { - fn from_constant(_constant: &'a Constant) -> Self { - todo!() +impl<'a> GritBinding<'a> { + pub fn from_tree(tree: &'a GritTree) -> Self { + Self::Tree(tree) + } +} + +impl<'a> Binding<'a, GritQueryContext> for GritBinding<'a> { + fn from_constant(constant: &'a Constant) -> Self { + Self::Constant(constant) } - fn from_node(_node: GritTargetNode) -> Self { - todo!() + fn from_node(node: GritTargetNode) -> Self { + Self::Node(node) } fn from_path(_path: &'a Path) -> Self { @@ -26,24 +37,61 @@ impl<'a> Binding<'a, GritQueryContext> for GritBinding { todo!() } + /// Returns the only node bound by this binding. + /// + /// This includes list bindings that only match a single child. + /// + /// Returns `None` if the binding has no associated node, or if there is + /// more than one associated node. fn singleton(&self) -> Option { - todo!() + match self { + Self::Node(node) => Some(node.clone()), + Self::Tree(..) | Self::Constant(..) => None, + } } fn get_sexp(&self) -> Option { - todo!() + None } fn position(&self, _language: &GritTargetLanguage) -> Option { - todo!() + match self { + GritBinding::Tree(tree) => { + let source = tree.source(); + let source = SourceFile::new(SourceCode { + text: &source, + line_starts: None, + }); + source.to_grit_range(tree.root_node().text_range()) + } + GritBinding::Node(node) => { + // TODO: This is probably very inefficient. + let root = node.ancestors().last()?; + let source = root.text().to_string(); + let source = SourceFile::new(SourceCode { + text: &source, + line_starts: None, + }); + source.to_grit_range(root.text_trimmed_range()) + } + GritBinding::Constant(_) => None, + } } fn range(&self, _language: &GritTargetLanguage) -> Option { - todo!() + match self { + GritBinding::Tree(tree) => Some(tree.root_node().byte_range()), + GritBinding::Node(node) => Some(node.byte_range()), + GritBinding::Constant(_) => None, + } } fn code_range(&self, _language: &GritTargetLanguage) -> Option { - todo!() + match self { + GritBinding::Tree(tree) => Some(tree.root_node().code_range()), + GritBinding::Node(node) => Some(node.code_range()), + GritBinding::Constant(_) => None, + } } fn is_equivalent_to(&self, _other: &Self, _language: &GritTargetLanguage) -> bool { diff --git a/crates/biome_grit_patterns/src/grit_code_snippet.rs b/crates/biome_grit_patterns/src/grit_code_snippet.rs index 7294670ddb20..f032b5ebbd3b 100644 --- a/crates/biome_grit_patterns/src/grit_code_snippet.rs +++ b/crates/biome_grit_patterns/src/grit_code_snippet.rs @@ -1,13 +1,17 @@ use crate::grit_context::{GritExecContext, GritQueryContext}; -use crate::resolved_pattern::GritResolvedPattern; +use crate::grit_resolved_pattern::GritResolvedPattern; +use crate::grit_target_node::GritTargetSyntaxKind; use anyhow::Result; +use grit_pattern_matcher::binding::Binding; +use grit_pattern_matcher::context::ExecContext; use grit_pattern_matcher::pattern::{ - CodeSnippet, DynamicPattern, Matcher, Pattern, PatternName, State, + CodeSnippet, DynamicPattern, Matcher, Pattern, PatternName, ResolvedPattern, State, }; use grit_util::AnalysisLogs; #[derive(Clone, Debug)] pub(crate) struct GritCodeSnippet { + pub(crate) patterns: Vec<(GritTargetSyntaxKind, Pattern)>, pub(crate) source: String, pub(crate) dynamic_snippet: Option>, } @@ -25,12 +29,24 @@ impl CodeSnippet for GritCodeSnippet { impl Matcher for GritCodeSnippet { fn execute<'a>( &'a self, - _binding: &GritResolvedPattern, - _state: &mut State<'a, GritQueryContext>, - _context: &'a GritExecContext, - _logs: &mut AnalysisLogs, + resolved: &GritResolvedPattern<'a>, + state: &mut State<'a, GritQueryContext>, + context: &'a GritExecContext, + logs: &mut AnalysisLogs, ) -> Result { - todo!() + let Some(binding) = resolved.get_last_binding() else { + return Ok(resolved.text(&state.files, context.language())?.trim() == self.source); + }; + + let Some(node) = binding.singleton() else { + return Ok(false); + }; + + if let Some((_, pattern)) = self.patterns.iter().find(|(kind, _)| *kind == node.kind()) { + pattern.execute(resolved, state, context, logs) + } else { + Ok(false) + } } } diff --git a/crates/biome_grit_patterns/src/grit_context.rs b/crates/biome_grit_patterns/src/grit_context.rs index d045454ebf96..5022ac9d002e 100644 --- a/crates/biome_grit_patterns/src/grit_context.rs +++ b/crates/biome_grit_patterns/src/grit_context.rs @@ -2,10 +2,10 @@ use crate::grit_binding::GritBinding; use crate::grit_code_snippet::GritCodeSnippet; use crate::grit_file::GritFile; use crate::grit_node_patterns::{GritLeafNodePattern, GritNodePattern}; +use crate::grit_resolved_pattern::GritResolvedPattern; use crate::grit_target_language::GritTargetLanguage; use crate::grit_target_node::GritTargetNode; use crate::grit_tree::GritTree; -use crate::resolved_pattern::GritResolvedPattern; use anyhow::Result; use grit_pattern_matcher::context::{ExecContext, QueryContext}; use grit_pattern_matcher::file_owners::FileOwners; @@ -14,7 +14,7 @@ use grit_pattern_matcher::pattern::{ }; use grit_util::AnalysisLogs; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct GritQueryContext; impl QueryContext for GritQueryContext { @@ -22,16 +22,24 @@ impl QueryContext for GritQueryContext { type NodePattern = GritNodePattern; type LeafNodePattern = GritLeafNodePattern; type ExecContext<'a> = GritExecContext; - type Binding<'a> = GritBinding; + type Binding<'a> = GritBinding<'a>; type CodeSnippet = GritCodeSnippet; - type ResolvedPattern<'a> = GritResolvedPattern; + type ResolvedPattern<'a> = GritResolvedPattern<'a>; type Language<'a> = GritTargetLanguage; - type File<'a> = GritFile; + type File<'a> = GritFile<'a>; type Tree<'a> = GritTree; } #[derive(Debug)] -pub(crate) struct GritExecContext; +pub(crate) struct GritExecContext { + lang: GritTargetLanguage, +} + +impl GritExecContext { + pub fn new(lang: GritTargetLanguage) -> Self { + Self { lang } + } +} impl<'a> ExecContext<'a, GritQueryContext> for GritExecContext { fn pattern_definitions(&self) -> &[PatternDefinition] { @@ -56,7 +64,7 @@ impl<'a> ExecContext<'a, GritQueryContext> for GritExecContext { _context: &'a Self, _state: &mut State<'a, GritQueryContext>, _logs: &mut AnalysisLogs, - ) -> Result { + ) -> Result> { todo!() } @@ -65,7 +73,7 @@ impl<'a> ExecContext<'a, GritQueryContext> for GritExecContext { } fn language(&self) -> &GritTargetLanguage { - todo!() + &self.lang } fn exec_step( diff --git a/crates/biome_grit_patterns/src/grit_file.rs b/crates/biome_grit_patterns/src/grit_file.rs index 30a2a9f775a5..f744e7fbd09e 100644 --- a/crates/biome_grit_patterns/src/grit_file.rs +++ b/crates/biome_grit_patterns/src/grit_file.rs @@ -1,28 +1,65 @@ +use std::path::Path; + use crate::grit_context::GritQueryContext; +use crate::grit_resolved_pattern::GritResolvedPattern; use crate::grit_target_language::GritTargetLanguage; -use crate::resolved_pattern::GritResolvedPattern; -use grit_pattern_matcher::pattern::{File, FileRegistry}; +use grit_pattern_matcher::{ + constant::Constant, + pattern::{File, FilePtr, FileRegistry, ResolvedFile, ResolvedPattern}, +}; +use grit_util::Ast; +use path_absolutize::Absolutize; -pub(crate) struct GritFile; +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum GritFile<'a> { + Resolved(Box>), + Ptr(FilePtr), +} -impl<'a> File<'a, GritQueryContext> for GritFile { - fn name(&self, _files: &FileRegistry<'a, GritQueryContext>) -> GritResolvedPattern { - todo!() +impl<'a> File<'a, GritQueryContext> for GritFile<'a> { + fn name(&self, files: &FileRegistry<'a, GritQueryContext>) -> GritResolvedPattern<'a> { + match self { + Self::Resolved(resolved) => resolved.name.clone(), + Self::Ptr(ptr) => GritResolvedPattern::from_path_binding(files.get_file_name(*ptr)), + } } fn absolute_path( &self, - _files: &FileRegistry<'a, GritQueryContext>, - _language: &GritTargetLanguage, - ) -> anyhow::Result { - todo!() + files: &FileRegistry<'a, GritQueryContext>, + language: &GritTargetLanguage, + ) -> anyhow::Result> { + match self { + Self::Resolved(resolved) => { + let name = resolved.name.text(files, language)?; + let absolute_path = Path::new(name.as_ref()).absolutize()?; + Ok(ResolvedPattern::from_constant(Constant::String( + absolute_path.to_string_lossy().to_string(), + ))) + } + Self::Ptr(ptr) => Ok(ResolvedPattern::from_path_binding( + files.get_absolute_path(*ptr)?, + )), + } } - fn body(&self, _files: &FileRegistry<'a, GritQueryContext>) -> GritResolvedPattern { - todo!() + fn body(&self, files: &FileRegistry<'a, GritQueryContext>) -> GritResolvedPattern<'a> { + match self { + Self::Resolved(resolved) => resolved.body.clone(), + Self::Ptr(ptr) => { + let file = &files.get_file_owner(*ptr); + GritResolvedPattern::from_tree(&file.tree) + } + } } - fn binding(&self, _files: &FileRegistry<'a, GritQueryContext>) -> GritResolvedPattern { - todo!() + fn binding(&self, files: &FileRegistry<'a, GritQueryContext>) -> GritResolvedPattern<'a> { + match self { + Self::Resolved(resolved) => resolved.body.clone(), + Self::Ptr(ptr) => { + let file = &files.get_file_owner(*ptr); + ResolvedPattern::from_node_binding(file.tree.root_node()) + } + } } } diff --git a/crates/biome_grit_patterns/src/grit_node_patterns.rs b/crates/biome_grit_patterns/src/grit_node_patterns.rs index 661974ef4db1..f24e884f132b 100644 --- a/crates/biome_grit_patterns/src/grit_node_patterns.rs +++ b/crates/biome_grit_patterns/src/grit_node_patterns.rs @@ -1,14 +1,17 @@ use crate::grit_context::{GritExecContext, GritQueryContext}; -use crate::grit_target_node::GritTargetNode; -use crate::resolved_pattern::GritResolvedPattern; +use crate::grit_resolved_pattern::GritResolvedPattern; +use crate::grit_target_node::{GritTargetNode, GritTargetSyntaxKind}; use anyhow::Result; use grit_pattern_matcher::pattern::{ - AstLeafNodePattern, AstNodePattern, Matcher, PatternName, PatternOrPredicate, State, + AstLeafNodePattern, AstNodePattern, Matcher, Pattern, PatternName, PatternOrPredicate, State, }; use grit_util::AnalysisLogs; #[derive(Clone, Debug)] -pub(crate) struct GritNodePattern; +pub(crate) struct GritNodePattern { + pub kind: GritTargetSyntaxKind, + pub args: Vec, +} impl AstNodePattern for GritNodePattern { const INCLUDES_TRIVIA: bool = true; @@ -41,9 +44,40 @@ impl PatternName for GritNodePattern { } #[derive(Clone, Debug)] -pub(crate) struct GritLeafNodePattern; +pub(crate) struct GritNodeArg { + slot_index: usize, + pattern: Pattern, +} -impl AstLeafNodePattern for GritLeafNodePattern {} +impl GritNodeArg { + pub fn new(slot_index: usize, pattern: Pattern) -> Self { + Self { + slot_index, + pattern, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct GritLeafNodePattern { + kind: GritTargetSyntaxKind, + text: String, +} + +impl GritLeafNodePattern { + pub fn new(kind: GritTargetSyntaxKind, text: impl Into) -> Self { + Self { + kind, + text: text.into(), + } + } +} + +impl AstLeafNodePattern for GritLeafNodePattern { + fn text(&self) -> Option<&str> { + Some(&self.text) + } +} impl Matcher for GritLeafNodePattern { fn execute<'a>( diff --git a/crates/biome_grit_patterns/src/grit_query.rs b/crates/biome_grit_patterns/src/grit_query.rs index f43b55404d7c..65398d855c4c 100644 --- a/crates/biome_grit_patterns/src/grit_query.rs +++ b/crates/biome_grit_patterns/src/grit_query.rs @@ -1,11 +1,12 @@ use crate::diagnostics::CompilerDiagnostic; use crate::grit_context::{GritExecContext, GritQueryContext}; +use crate::grit_resolved_pattern::GritResolvedPattern; use crate::grit_target_language::GritTargetLanguage; +use crate::grit_tree::GritTree; use crate::pattern_compiler::PatternCompiler; use crate::pattern_compiler::{ compilation_context::CompilationContext, compilation_context::NodeCompilationContext, }; -use crate::resolved_pattern::GritResolvedPattern; use crate::variables::{VarRegistry, VariableLocations}; use crate::CompileError; use anyhow::Result; @@ -19,19 +20,21 @@ use std::collections::BTreeMap; pub struct GritQuery { pub(crate) pattern: Pattern, + /// Context for executing the query. + context: GritExecContext, + /// Diagnostics discovered during compilation of the query. diagnostics: Vec, /// All variables discovered during query compilation. - locations: VariableLocations, + variables: VariableLocations, } impl GritQuery { - pub fn execute(&self) -> Result { - let var_registry = VarRegistry::from_locations(&self.locations); + pub fn execute(&self, tree: &GritTree) -> Result { + let var_registry = VarRegistry::from_locations(&self.variables); - let binding = GritResolvedPattern; - let context = GritExecContext; + let binding = GritResolvedPattern::from_tree(tree); let mut state = State::new( var_registry.into(), FileRegistry::new_from_paths(Vec::new()), @@ -39,7 +42,7 @@ impl GritQuery { let mut logs = Vec::new().into(); self.pattern - .execute(&binding, &mut state, &context, &mut logs) + .execute(&binding, &mut state, &self.context, &mut logs) } pub fn from_node(root: GritRoot, lang: GritTargetLanguage) -> Result { @@ -66,12 +69,14 @@ impl GritQuery { &mut node_context, )?; + let context = GritExecContext::new(context.lang); let locations = VariableLocations::new(vars_array); Ok(Self { pattern, + context, diagnostics, - locations, + variables: locations, }) } } diff --git a/crates/biome_grit_patterns/src/resolved_pattern.rs b/crates/biome_grit_patterns/src/grit_resolved_pattern.rs similarity index 64% rename from crates/biome_grit_patterns/src/resolved_pattern.rs rename to crates/biome_grit_patterns/src/grit_resolved_pattern.rs index 99710d00cad7..cc28546e9447 100644 --- a/crates/biome_grit_patterns/src/resolved_pattern.rs +++ b/crates/biome_grit_patterns/src/grit_resolved_pattern.rs @@ -1,23 +1,42 @@ use crate::grit_context::GritExecContext; +use crate::grit_file::GritFile; +use crate::grit_tree::GritTree; use crate::{grit_binding::GritBinding, grit_context::GritQueryContext}; use anyhow::Result; +use grit_pattern_matcher::binding::Binding; use grit_pattern_matcher::constant::Constant; +use grit_pattern_matcher::context::QueryContext; use grit_pattern_matcher::effects::Effect; use grit_pattern_matcher::pattern::{ Accessor, DynamicPattern, DynamicSnippet, FilePtr, FileRegistry, ListIndex, Pattern, ResolvedPattern, ResolvedSnippet, State, }; use grit_util::{AnalysisLogs, CodeRange, Range}; -use im::Vector; +use im::{vector, Vector}; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; #[derive(Clone, Debug, PartialEq)] -pub(crate) struct GritResolvedPattern; +pub(crate) enum GritResolvedPattern<'a> { + Binding(Vector>), + Snippets(Vector>), + List(Vector>), + Map(BTreeMap>), + File(GritFile<'a>), + Files(Box>), + Constant(Constant), + Tree(GritTree), +} -impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { - fn from_binding(_binding: GritBinding) -> Self { - todo!() +impl<'a> GritResolvedPattern<'a> { + pub fn from_tree(tree: &'a GritTree) -> Self { + Self::from_binding(GritBinding::from_tree(tree)) + } +} + +impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern<'a> { + fn from_binding(binding: GritBinding<'a>) -> Self { + Self::Binding(vector![binding]) } fn from_constant(_constant: Constant) -> Self { @@ -93,7 +112,7 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { &mut self, _with: Self, _effects: &mut Vector>, - _language: &::Language<'a>, + _language: &::Language<'a>, ) -> anyhow::Result<()> { todo!() } @@ -101,19 +120,25 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { fn float( &self, _state: &FileRegistry<'a, GritQueryContext>, - _language: &::Language<'a>, + _language: &::Language<'a>, ) -> anyhow::Result { todo!() } - fn get_bindings(&self) -> Option> { - None:: + fn get_bindings(&self) -> Option>> { + if let Self::Binding(bindings) = self { + Some(bindings.iter().cloned()) + } else { + None + } } - fn get_file( - &self, - ) -> Option<&::File<'a>> { - todo!() + fn get_file(&self) -> Option<&GritFile<'a>> { + if let Self::File(file) = self { + Some(file) + } else { + None + } } fn get_file_pointers(&self) -> Option> { @@ -124,8 +149,12 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { todo!() } - fn get_last_binding(&self) -> Option<&GritBinding> { - todo!() + fn get_last_binding(&self) -> Option<&GritBinding<'a>> { + if let Self::Binding(bindings) = self { + bindings.last() + } else { + None + } } fn get_list_item_at(&self, _index: isize) -> Option<&Self> { @@ -137,44 +166,62 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { } fn get_list_items(&self) -> Option> { - None:: + if let Self::List(items) = self { + Some(items.iter()) + } else { + None + } } fn get_list_binding_items(&self) -> Option + Clone> { - None:: + self.get_last_binding() + .and_then(Binding::list_items) + .map(|items| items.map(GritResolvedPattern::from_node_binding)) } fn get_map(&self) -> Option<&std::collections::BTreeMap> { - todo!() + if let Self::Map(map) = self { + Some(map) + } else { + None + } } fn get_map_mut(&mut self) -> Option<&mut std::collections::BTreeMap> { - todo!() + if let Self::Map(map) = self { + Some(map) + } else { + None + } } fn get_snippets(&self) -> Option>> { - None:: + if let Self::Snippets(snippets) = self { + Some(snippets.iter().cloned()) + } else { + None + } } fn is_binding(&self) -> bool { - todo!() + matches!(self, Self::Binding(_)) } fn is_list(&self) -> bool { - todo!() + matches!(self, Self::List(_)) } fn is_truthy( &self, _state: &mut State<'a, GritQueryContext>, - _language: &::Language<'a>, + _language: &::Language<'a>, ) -> Result { todo!() } fn linearized_text( &self, - _language: &::Language<'a>, + _language: &::Language<'a>, _effects: &[Effect<'a, GritQueryContext>], _files: &FileRegistry<'a, GritQueryContext>, _memo: &mut HashMap>, @@ -196,14 +243,14 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { &mut self, _binding: &GritBinding, _is_first: bool, - _language: &::Language<'a>, + _language: &::Language<'a>, ) -> Result<()> { todo!() } fn position( &self, - _language: &::Language<'a>, + _language: &::Language<'a>, ) -> Option { todo!() } @@ -219,17 +266,19 @@ impl<'a> ResolvedPattern<'a, GritQueryContext> for GritResolvedPattern { fn text( &self, _state: &grit_pattern_matcher::pattern::FileRegistry<'a, GritQueryContext>, - _language: &::Language<'a>, + _language: &::Language<'a>, ) -> Result> { todo!() } } #[derive(Clone)] -struct TodoBindingIterator; +struct TodoBindingIterator<'a> { + _pattern: &'a GritResolvedPattern<'a>, +} -impl Iterator for TodoBindingIterator { - type Item = GritBinding; +impl<'a> Iterator for TodoBindingIterator<'a> { + type Item = GritBinding<'a>; fn next(&mut self) -> Option { todo!() @@ -237,10 +286,12 @@ impl Iterator for TodoBindingIterator { } #[derive(Clone)] -struct TodoSelfIterator; +struct TodoSelfIterator<'a> { + _pattern: &'a GritResolvedPattern<'a>, +} -impl Iterator for TodoSelfIterator { - type Item = GritResolvedPattern; +impl<'a> Iterator for TodoSelfIterator<'a> { + type Item = GritResolvedPattern<'a>; fn next(&mut self) -> Option { todo!() @@ -248,11 +299,11 @@ impl Iterator for TodoSelfIterator { } struct TodoSelfRefIterator<'a> { - _pattern: &'a GritResolvedPattern, + _pattern: &'a GritResolvedPattern<'a>, } impl<'a> Iterator for TodoSelfRefIterator<'a> { - type Item = &'a GritResolvedPattern; + type Item = &'a GritResolvedPattern<'a>; fn next(&mut self) -> Option { todo!() @@ -261,7 +312,7 @@ impl<'a> Iterator for TodoSelfRefIterator<'a> { #[derive(Clone)] struct TodoSnippetIterator<'a> { - _pattern: &'a GritResolvedPattern, + _pattern: &'a GritResolvedPattern<'a>, } impl<'a> Iterator for TodoSnippetIterator<'a> { diff --git a/crates/biome_grit_patterns/src/grit_target_node.rs b/crates/biome_grit_patterns/src/grit_target_node.rs index 733d57e78db0..2c120fa76d25 100644 --- a/crates/biome_grit_patterns/src/grit_target_node.rs +++ b/crates/biome_grit_patterns/src/grit_target_node.rs @@ -1,6 +1,6 @@ use crate::util::TextRangeGritExt; -use biome_js_syntax::{JsSyntaxKind, JsSyntaxNode, JsSyntaxToken}; -use biome_rowan::{SyntaxKind, SyntaxNodeText, TextRange}; +use biome_js_syntax::{JsLanguage, JsSyntaxKind, JsSyntaxNode, JsSyntaxToken}; +use biome_rowan::{SyntaxKind, SyntaxNodeText, SyntaxSlot, TextRange}; use grit_util::{AstCursor, AstNode as GritAstNode, ByteRange, CodeRange}; use std::{borrow::Cow, str::Utf8Error}; @@ -25,7 +25,7 @@ use std::{borrow::Cow, str::Utf8Error}; /// for this, it may allow us to one day query CSS rules inside a JS template /// literal, for instance. macro_rules! generate_target_node { - ($([$lang_node:ident, $lang_token:ident, $lang_kind:ident]),+) => { + ($([$lang:ident, $lang_node:ident, $lang_token:ident, $lang_kind:ident]),+) => { #[derive(Clone, Debug, PartialEq)] pub enum GritTargetNode { $($lang_node($lang_node)),+ @@ -38,30 +38,54 @@ macro_rules! generate_target_node { })+ impl GritTargetNode { - fn first_child(&self) -> Option { + pub fn first_child(&self) -> Option { match self { $(Self::$lang_node(node) => node.first_child().map(Into::into)),+ } } - fn first_token(&self) -> Option { + pub fn first_token(&self) -> Option { match self { $(Self::$lang_node(node) => node.first_token().map(Into::into)),+ } } + pub fn has_children(&self) -> bool { + match self { + $(Self::$lang_node(node) => node.first_child().is_some()),+ + } + } + + pub fn index(&self) -> usize { + match self { + $(Self::$lang_node(node) => node.index()),+ + } + } + pub fn kind(&self) -> GritTargetSyntaxKind { match self { $(Self::$lang_node(node) => node.kind().into()),+ } } + pub fn slots(&self) -> impl Iterator { + match self { + $(Self::$lang_node(node) => node.slots().map(Into::into)),+ + } + } + pub fn text(&self) -> SyntaxNodeText { match self { $(Self::$lang_node(node) => node.text()),+ } } + pub fn text_range(&self) -> TextRange { + match self { + $(Self::$lang_node(node) => node.text_range()),+ + } + } + pub fn text_trimmed(&self) -> SyntaxNodeText { match self { $(Self::$lang_node(node) => node.text_trimmed()),+ @@ -157,7 +181,7 @@ macro_rules! generate_target_node { } } - #[derive(Clone, Debug)] + #[derive(Clone, Debug, PartialEq)] pub enum GritTargetToken { $($lang_token($lang_token)),+ } @@ -169,7 +193,19 @@ macro_rules! generate_target_node { })+ impl GritTargetToken { - fn text(&self) -> &str { + pub fn index(&self) -> usize { + match self { + $(Self::$lang_token(token) => token.index()),+ + } + } + + pub fn kind(&self) -> GritTargetSyntaxKind { + match self { + $(Self::$lang_token(token) => token.kind().into()),+ + } + } + + pub fn text(&self) -> &str { match self { $(Self::$lang_token(token) => token.text()),+ } @@ -193,12 +229,34 @@ macro_rules! generate_target_node { $(Self::$lang_kind(kind) => kind.is_bogus()),+ } } + + pub fn is_list(&self) -> bool { + match self { + $(Self::$lang_kind(kind) => kind.is_list()),+ + } + } + + pub fn is_token(&self) -> bool { + match self { + $(Self::$lang_kind(kind) => kind.is_punct() || kind.is_literal()),+ + } + } } + + $(impl From> for GritSyntaxSlot { + fn from(slot: SyntaxSlot<$lang>) -> Self { + match slot { + SyntaxSlot::Node(node) => Self::Node(node.into()), + SyntaxSlot::Token(token) => Self::Token(token.into()), + SyntaxSlot::Empty { index } => Self::Empty { index }, + } + } + })+ }; } generate_target_node! { - [JsSyntaxNode, JsSyntaxToken, JsSyntaxKind] + [JsLanguage, JsSyntaxNode, JsSyntaxToken, JsSyntaxKind] } impl GritTargetSyntaxKind { @@ -209,6 +267,33 @@ impl GritTargetSyntaxKind { } } +#[derive(Clone, Debug, PartialEq)] +pub enum GritSyntaxSlot { + /// Slot that stores a node child + Node(GritTargetNode), + /// Slot that stores a token child + Token(GritTargetToken), + /// Slot that marks that the child in this position isn't present in the source code. + Empty { index: u32 }, +} + +impl GritSyntaxSlot { + pub fn contains_list(&self) -> bool { + match self { + GritSyntaxSlot::Node(node) => node.kind().is_list(), + GritSyntaxSlot::Token(_) | GritSyntaxSlot::Empty { .. } => false, + } + } + + pub fn index(&self) -> usize { + match self { + GritSyntaxSlot::Node(node) => node.index(), + GritSyntaxSlot::Token(token) => token.index(), + GritSyntaxSlot::Empty { index } => *index as usize, + } + } +} + #[derive(Clone)] pub struct AncestorIterator { node: Option, @@ -261,11 +346,15 @@ impl Iterator for ChildrenIterator { #[derive(Clone)] struct GritTargetNodeCursor { node: GritTargetNode, + root: GritTargetNode, } impl GritTargetNodeCursor { fn new(node: &GritTargetNode) -> Self { - Self { node: node.clone() } + Self { + node: node.clone(), + root: node.clone(), + } } } @@ -283,6 +372,9 @@ impl AstCursor for GritTargetNodeCursor { } fn goto_parent(&mut self) -> bool { + if self.node == self.root { + return false; + } match self.node.parent() { Some(parent) => { self.node = parent; diff --git a/crates/biome_grit_patterns/src/lib.rs b/crates/biome_grit_patterns/src/lib.rs index 40b8d76ed8a8..21e165c75754 100644 --- a/crates/biome_grit_patterns/src/lib.rs +++ b/crates/biome_grit_patterns/src/lib.rs @@ -10,11 +10,11 @@ mod grit_js_parser; mod grit_node; mod grit_node_patterns; mod grit_query; +mod grit_resolved_pattern; mod grit_target_language; mod grit_target_node; mod grit_tree; mod pattern_compiler; -mod resolved_pattern; mod source_location_ext; mod util; mod variables; @@ -22,6 +22,7 @@ mod variables; pub use errors::*; pub use grit_query::GritQuery; pub use grit_target_language::{GritTargetLanguage, JsTargetLanguage}; +pub use grit_tree::GritTree; use biome_grit_parser::parse_grit; diff --git a/crates/biome_grit_patterns/src/pattern_compiler.rs b/crates/biome_grit_patterns/src/pattern_compiler.rs index db04676f3b92..53ce2534e598 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler.rs @@ -63,6 +63,8 @@ mod variable_compiler; mod where_compiler; mod within_compiler; +use std::collections::BTreeMap; + use self::{ accumulate_compiler::AccumulateCompiler, add_compiler::AddCompiler, after_compiler::AfterCompiler, and_compiler::AndCompiler, any_compiler::AnyCompiler, @@ -79,10 +81,19 @@ use self::{ subtract_compiler::SubtractCompiler, variable_compiler::VariableCompiler, where_compiler::WhereCompiler, within_compiler::WithinCompiler, }; -use crate::{grit_context::GritQueryContext, CompileError}; +use crate::{ + grit_context::GritQueryContext, + grit_node_patterns::{GritLeafNodePattern, GritNodeArg, GritNodePattern}, + grit_target_node::{GritSyntaxSlot, GritTargetNode, GritTargetToken}, + CompileError, GritTargetLanguage, +}; use biome_grit_syntax::{AnyGritMaybeCurlyPattern, AnyGritPattern, GritSyntaxKind}; -use biome_rowan::AstNode; -use grit_pattern_matcher::pattern::{DynamicPattern, DynamicSnippet, DynamicSnippetPart, Pattern}; +use biome_rowan::AstNode as _; +use grit_pattern_matcher::pattern::{ + is_reserved_metavariable, DynamicPattern, DynamicSnippet, DynamicSnippetPart, List, Pattern, + RegexLike, RegexPattern, Variable, +}; +use grit_util::{traverse, AstNode, ByteRange, GritMetaValue, Language, Order}; pub(crate) struct PatternCompiler; @@ -226,3 +237,262 @@ impl PatternCompiler { } } } + +impl PatternCompiler { + pub(crate) fn from_snippet_node( + node: GritTargetNode, + context_range: ByteRange, + context: &mut NodeCompilationContext, + is_rhs: bool, + ) -> Result, CompileError> { + let snippet_start = node.text().char_at(0.into()).unwrap_or_default() as usize; + let ranges = metavariable_ranges(&node, &context.compilation.lang); + let range_map = metavariable_range_mapping(ranges, snippet_start); + + fn node_to_pattern( + node: GritTargetNode, + context_range: ByteRange, + range_map: &BTreeMap, + context: &mut NodeCompilationContext, + is_rhs: bool, + ) -> anyhow::Result, CompileError> { + let metavariable = + metavariable_descendent(&node, context_range, range_map, context, is_rhs)?; + if let Some(metavariable) = metavariable { + return Ok(metavariable); + } + + let kind = node.kind(); + if !node.has_children() { + if let Some(token) = node.first_token() { + let content = token.text(); + if context + .compilation + .lang + .replaced_metavariable_regex() + .is_match(content) + { + let regex = + implicit_metavariable_regex(&token, context_range, range_map, context)?; + if let Some(regex) = regex { + return Ok(Pattern::Regex(Box::new(regex))); + } + } + + return Ok(Pattern::AstLeafNode(GritLeafNodePattern::new( + kind, content, + ))); + } + } + + let args: Vec = node + .slots() + // TODO: Implement filtering for disregarded snippet fields. + // Implementing this will make it more convenient to match + // CST nodes without needing to match all the trivia in the + // snippet. + .map(|slot| { + let mut nodes_list: Vec> = match &slot { + GritSyntaxSlot::Node(node) => node + .children() + .map(|n| node_to_pattern(n, context_range, range_map, context, is_rhs)) + .collect::>()?, + _ => Vec::new(), + }; + if !slot.contains_list() { + Ok(GritNodeArg::new( + slot.index(), + nodes_list + .pop() + .unwrap_or(Pattern::Dynamic(DynamicPattern::Snippet( + DynamicSnippet { + parts: vec![DynamicSnippetPart::String(String::new())], + }, + ))), + )) + } else if nodes_list.len() == 1 + && matches!( + nodes_list.first(), + Some(Pattern::Variable(_) | Pattern::Underscore) + ) + { + Ok(GritNodeArg::new(slot.index(), nodes_list.pop().unwrap())) + } else { + Ok(GritNodeArg::new( + slot.index(), + Pattern::List(Box::new(List::new(nodes_list))), + )) + } + }) + .collect::>()?; + Ok(Pattern::AstNode(Box::new(GritNodePattern { kind, args }))) + } + node_to_pattern(node, context_range, &range_map, context, is_rhs) + } +} + +fn implicit_metavariable_regex( + token: &GritTargetToken, + context_range: ByteRange, + range_map: &BTreeMap, + context: &mut NodeCompilationContext, +) -> Result>, CompileError> { + let source = token.text(); + let capture_string = "(.*)"; + let uncapture_string = ".*"; + let variable_regex = context.compilation.lang.replaced_metavariable_regex(); + let mut last = 0; + let mut regex_string = String::new(); + let mut variables: Vec = vec![]; + for m in variable_regex.find_iter(source) { + regex_string.push_str(®ex::escape(&source[last..m.start()])); + let range = ByteRange::new(m.start(), m.end()); + last = range.end; + let name = m.as_str(); + let variable = text_to_var(name, range, context_range, range_map, context)?; + match variable { + SnippetValues::Dots => return Ok(None), + SnippetValues::Underscore => regex_string.push_str(uncapture_string), + SnippetValues::Variable(var) => { + regex_string.push_str(capture_string); + variables.push(var); + } + } + } + + if last < source.len() { + regex_string.push_str(®ex::escape(&source[last..])); + } + let regex = regex_string.to_string(); + let regex = RegexLike::Regex(regex); + Ok(Some(RegexPattern::new(regex, variables))) +} + +fn metavariable_descendent( + node: &GritTargetNode, + context_range: ByteRange, + range_map: &BTreeMap, + context: &mut NodeCompilationContext, + is_rhs: bool, +) -> Result>, CompileError> { + let Some(token) = node.first_token() else { + return Ok(None); + }; + if !context.compilation.lang.is_metavariable(node) { + return Ok(None); + } + + let name = token.text(); + if is_reserved_metavariable(name, Some(&context.compilation.lang)) && !is_rhs { + return Err(CompileError::ReservedMetavariable( + name.trim_start_matches(context.compilation.lang.metavariable_prefix_substitute()) + .to_string(), + )); + } + + let range = node.byte_range(); + text_to_var(name, range, context_range, range_map, context).map(|s| Some(s.into())) +} + +fn metavariable_ranges(node: &GritTargetNode, lang: &GritTargetLanguage) -> Vec { + let cursor = node.walk(); + traverse(cursor, Order::Pre) + .flat_map(|child| { + if lang.is_metavariable(&child) { + vec![child.byte_range()] + } else { + node_sub_variables(&child, lang) + } + }) + .collect() +} + +// assumes that metavariable substitute is 1 byte larger than the original. eg. +// len(ยต) = 2 bytes, len($) = 1 byte +fn metavariable_range_mapping( + mut ranges: Vec, + snippet_offset: usize, +) -> BTreeMap { + // assumes metavariable ranges do not enclose one another + ranges.sort_by_key(|r| r.start); + + let mut byte_offset = snippet_offset; + let mut map = BTreeMap::new(); + for range in ranges { + let start_byte = range.start - byte_offset; + if !cfg!(target_arch = "wasm32") { + byte_offset += 1; + } + + let end_byte = range.end - byte_offset; + let new_range = ByteRange::new(start_byte, end_byte); + map.insert(range, new_range); + } + + map +} + +fn node_sub_variables(node: &GritTargetNode, lang: &impl Language) -> Vec { + let mut ranges = vec![]; + if node.has_children() { + return ranges; + } + + let Some(token) = node.first_token() else { + return ranges; + }; + + let source = token.text(); + let variable_regex = lang.replaced_metavariable_regex(); + for m in variable_regex.find_iter(source) { + let var_range = ByteRange::new(m.start(), m.end()); + let start_byte = node.start_byte() as usize; + let end_byte = node.end_byte() as usize; + if var_range.start >= start_byte && var_range.end <= end_byte { + ranges.push(var_range); + } + } + + ranges +} + +enum SnippetValues { + Dots, + Underscore, + Variable(Variable), +} + +impl From for Pattern { + fn from(value: SnippetValues) -> Self { + match value { + SnippetValues::Dots => Pattern::Dots, + SnippetValues::Underscore => Pattern::Underscore, + SnippetValues::Variable(v) => Pattern::Variable(v), + } + } +} + +fn text_to_var( + name: &str, + range: ByteRange, + context_range: ByteRange, + range_map: &BTreeMap, + context: &mut NodeCompilationContext, +) -> Result { + let name = context + .compilation + .lang + .snippet_metavariable_to_grit_metavariable(name) + .ok_or_else(|| CompileError::MetavariableNotFound(name.to_string()))?; + match name { + GritMetaValue::Dots => Ok(SnippetValues::Dots), + GritMetaValue::Underscore => Ok(SnippetValues::Underscore), + GritMetaValue::Variable(name) => { + let range = *range_map + .get(&range) + .ok_or_else(|| CompileError::InvalidMetavariableRange(range))?; + let var = context.register_variable(name, range + context_range.start)?; + Ok(SnippetValues::Variable(var)) + } + } +} diff --git a/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs b/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs index a166161a2719..a2f4e084eb29 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs @@ -1,7 +1,10 @@ -use super::compilation_context::NodeCompilationContext; +use super::{compilation_context::NodeCompilationContext, PatternCompiler}; use crate::{ - grit_code_snippet::GritCodeSnippet, grit_context::GritQueryContext, - grit_target_node::GritTargetNode, grit_tree::GritTree, CompileError, + grit_code_snippet::GritCodeSnippet, + grit_context::GritQueryContext, + grit_target_node::{GritTargetNode, GritTargetSyntaxKind}, + grit_tree::GritTree, + CompileError, }; use grit_pattern_matcher::{ constants::GLOBAL_VARS_SCOPE_INDEX, @@ -57,7 +60,7 @@ pub(crate) fn parse_snippet_content( } let snippet_trees = context.compilation.lang.parse_snippet_contexts(source); - let snippet_nodes = nodes_from_indices(&snippet_trees); + let snippet_nodes = nodes_from_trees(&snippet_trees); if snippet_nodes.is_empty() { // not checking if is_rhs. So could potentially // be harder to find bugs where we expect the pattern @@ -68,9 +71,19 @@ pub(crate) fn parse_snippet_content( )); } + let patterns: Vec<(GritTargetSyntaxKind, Pattern)> = snippet_nodes + .into_iter() + .map(|node| { + Ok(( + node.kind(), + PatternCompiler::from_snippet_node(node, range, context, is_rhs)?, + )) + }) + .collect::>()?; let dynamic_snippet = dynamic_snippet_from_source(source, range, context) .map_or(None, |s| Some(DynamicPattern::Snippet(s))); Ok(Pattern::CodeSnippet(GritCodeSnippet { + patterns, dynamic_snippet, source: source.to_owned(), })) @@ -127,14 +140,11 @@ pub(crate) fn dynamic_snippet_from_source( Ok(DynamicSnippet { parts }) } -pub fn nodes_from_indices(indices: &[SnippetTree]) -> Vec { - indices - .iter() - .filter_map(snippet_nodes_from_index) - .collect() +pub fn nodes_from_trees(indices: &[SnippetTree]) -> Vec { + indices.iter().filter_map(snippet_node_from_tree).collect() } -fn snippet_nodes_from_index(snippet: &SnippetTree) -> Option { +fn snippet_node_from_tree(snippet: &SnippetTree) -> Option { let mut snippet_root = snippet.tree.root_node(); // find the outermost node with the same index as the snippet diff --git a/crates/biome_grit_patterns/tests/quick_test.rs b/crates/biome_grit_patterns/tests/quick_test.rs index 6b515c523141..d6c1a7740626 100644 --- a/crates/biome_grit_patterns/tests/quick_test.rs +++ b/crates/biome_grit_patterns/tests/quick_test.rs @@ -1,23 +1,42 @@ use biome_grit_parser::parse_grit; -use biome_grit_patterns::{GritQuery, GritTargetLanguage, JsTargetLanguage}; +use biome_grit_patterns::{GritQuery, GritTargetLanguage, GritTree, JsTargetLanguage}; +use biome_js_parser::{parse_module, JsParserOptions}; // Use this test to quickly execute a Grit query against an source snippet. #[ignore] #[test] fn test_query() { - let parse_result = parse_grit("`console.log('hello')`"); - if !parse_result.diagnostics().is_empty() { + let parse_grit_result = parse_grit("`console.log('hello')`"); + if !parse_grit_result.diagnostics().is_empty() { println!( "Diagnostics from parsing query:\n{:?}", - parse_result.diagnostics() + parse_grit_result.diagnostics() ); } let query = GritQuery::from_node( - parse_result.tree(), + parse_grit_result.tree(), GritTargetLanguage::JsTargetLanguage(JsTargetLanguage), ) .expect("could not construct query"); - query.execute().expect("could not execute query"); + let parse_js_result = parse_module( + r#" +function hello() { + console + .log("hello"); +} +"#, + JsParserOptions::default(), + ); + if !parse_js_result.diagnostics().is_empty() { + println!( + "Diagnostics from parsing JS snippet:\n{:?}", + parse_js_result.diagnostics() + ); + } + + query + .execute(&GritTree::new(parse_js_result.syntax().into())) + .expect("could not execute query"); } diff --git a/crates/biome_rowan/src/syntax/token.rs b/crates/biome_rowan/src/syntax/token.rs index 5746dfb95db5..aacd8ecd6c28 100644 --- a/crates/biome_rowan/src/syntax/token.rs +++ b/crates/biome_rowan/src/syntax/token.rs @@ -69,7 +69,7 @@ impl SyntaxToken { self.raw.text_trimmed_range() } - pub(crate) fn index(&self) -> usize { + pub fn index(&self) -> usize { self.raw.index() }