From eac93bfea92c3dcaf07592ce80476280f2598391 Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Mon, 3 Jun 2024 20:48:26 +0200 Subject: [PATCH 1/2] Parse Grit literal snippets --- Cargo.lock | 9 +- crates/biome_grit_patterns/Cargo.toml | 5 +- crates/biome_grit_patterns/src/errors.rs | 7 + .../src/grit_analysis_ext.rs | 69 +++++++ .../biome_grit_patterns/src/grit_context.rs | 11 +- .../biome_grit_patterns/src/grit_js_parser.rs | 53 +++++ crates/biome_grit_patterns/src/grit_node.rs | 6 - .../src/grit_node_patterns.rs | 2 + crates/biome_grit_patterns/src/grit_query.rs | 7 +- .../src/grit_target_language.rs | 46 ++++- .../src/grit_target_node.rs | 24 ++- crates/biome_grit_patterns/src/grit_tree.rs | 13 +- crates/biome_grit_patterns/src/lib.rs | 2 + .../src/pattern_compiler/literal_compiler.rs | 18 +- .../src/pattern_compiler/snippet_compiler.rs | 187 +++++++++++++++++- .../biome_grit_patterns/tests/quick_test.rs | 23 +++ 16 files changed, 450 insertions(+), 32 deletions(-) create mode 100644 crates/biome_grit_patterns/src/grit_analysis_ext.rs create mode 100644 crates/biome_grit_patterns/src/grit_js_parser.rs create mode 100644 crates/biome_grit_patterns/tests/quick_test.rs diff --git a/Cargo.lock b/Cargo.lock index 9621e48e0528..55fac79d068a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,6 +540,7 @@ dependencies = [ "biome_diagnostics", "biome_grit_parser", "biome_grit_syntax", + "biome_js_parser", "biome_js_syntax", "biome_parser", "biome_rowan", @@ -1878,9 +1879,9 @@ dependencies = [ [[package]] name = "grit-pattern-matcher" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ca790933b1ae99fbb0caf4b90255bbcdc6507b893d5bbd8d0552bbb48018c2" +checksum = "eb91ad25bb8557f3be40899becf5db27e8431e359d69d5b67104f1d1a12c8d9a" dependencies = [ "anyhow", "elsa", @@ -1893,9 +1894,9 @@ dependencies = [ [[package]] name = "grit-util" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b466e27499052b6206a3fe31eff49c5c55586ea23f9d73b2df41c0de8057ef" +checksum = "e4d302f12945a38d464183a6ffe49b8b778876f82ab753174324cd7aae74b24a" dependencies = [ "derive_builder", "once_cell", diff --git a/crates/biome_grit_patterns/Cargo.toml b/crates/biome_grit_patterns/Cargo.toml index fee5dd73d4d3..50b1271f23d9 100644 --- a/crates/biome_grit_patterns/Cargo.toml +++ b/crates/biome_grit_patterns/Cargo.toml @@ -17,11 +17,12 @@ biome_console = { workspace = true } biome_diagnostics = { workspace = true } biome_grit_parser = { workspace = true } biome_grit_syntax = { workspace = true } +biome_js_parser = { workspace = true } biome_js_syntax = { workspace = true } biome_parser = { workspace = true } biome_rowan = { workspace = true } -grit-pattern-matcher = { version = "0.2" } -grit-util = { version = "0.2" } +grit-pattern-matcher = { version = "0.3" } +grit-util = { version = "0.3" } im = { version = "15.1.0" } 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 2591338ca1ee..bf3221577e6d 100644 --- a/crates/biome_grit_patterns/src/errors.rs +++ b/crates/biome_grit_patterns/src/errors.rs @@ -51,6 +51,13 @@ pub enum CompileError { /// A pattern is required to compile a Grit query. MissingPattern, + + /// Bracketed metavariables are only allowed on the right-hand side of + /// rewrite. + InvalidBracketedMetavariable, + + /// Unknown variable. + UnknownVariable(String), } impl Diagnostic for CompileError {} diff --git a/crates/biome_grit_patterns/src/grit_analysis_ext.rs b/crates/biome_grit_patterns/src/grit_analysis_ext.rs new file mode 100644 index 000000000000..f51f6459b5b5 --- /dev/null +++ b/crates/biome_grit_patterns/src/grit_analysis_ext.rs @@ -0,0 +1,69 @@ +use biome_diagnostics::{Diagnostic, PrintDescription, Severity}; +use grit_util::{AnalysisLog, Position, Range}; +use std::path::{Path, PathBuf}; + +pub trait GritAnalysisExt { + fn to_log(&self, path: Option<&Path>) -> AnalysisLog; +} + +impl GritAnalysisExt for T +where + T: Diagnostic, +{ + fn to_log(&self, path: Option<&Path>) -> AnalysisLog { + let location = self.location(); + let source = location.source_code; + let range = + match (location.span, source) { + (Some(range), Some(source)) => source.text[..range.start().into()] + .lines() + .enumerate() + .last() + .map(|(i, line)| { + let start = Position { + line: (i + 1) as u32, + column: line.len() as u32, + }; + let end = source.text[range].lines().enumerate().last().map_or( + start, + |(j, line)| Position { + line: start.line + j as u32, + column: if j == 0 { + start.column + line.len() as u32 + } else { + line.len() as u32 + }, + }, + ); + Range { + start, + end, + start_byte: range.start().into(), + end_byte: range.end().into(), + } + }), + _ => None, + }; + + AnalysisLog { + engine_id: Some("biome".to_owned()), + file: path.map(Path::to_path_buf).or_else(|| { + location + .resource + .and_then(|r| r.as_file().map(PathBuf::from)) + }), + level: Some(match self.severity() { + Severity::Hint => 1, + Severity::Information => 2, + Severity::Warning => 3, + Severity::Error => 4, + Severity::Fatal => 5, + }), + message: PrintDescription(self).to_string(), + position: range.as_ref().map(|r| r.start), + range, + syntax_tree: None, + source: source.map(|s| s.text.to_owned()), + } + } +} diff --git a/crates/biome_grit_patterns/src/grit_context.rs b/crates/biome_grit_patterns/src/grit_context.rs index f11de991d7ba..d045454ebf96 100644 --- a/crates/biome_grit_patterns/src/grit_context.rs +++ b/crates/biome_grit_patterns/src/grit_context.rs @@ -27,7 +27,7 @@ impl QueryContext for GritQueryContext { type ResolvedPattern<'a> = GritResolvedPattern; type Language<'a> = GritTargetLanguage; type File<'a> = GritFile; - type Tree = GritTree; + type Tree<'a> = GritTree; } #[derive(Debug)] @@ -81,4 +81,13 @@ impl<'a> ExecContext<'a, GritQueryContext> for GritExecContext { fn name(&self) -> Option<&str> { todo!() } + + fn load_file( + &self, + _file: &::File<'a>, + _state: &mut State<'a, GritQueryContext>, + _logs: &mut AnalysisLogs, + ) -> Result { + todo!() + } } diff --git a/crates/biome_grit_patterns/src/grit_js_parser.rs b/crates/biome_grit_patterns/src/grit_js_parser.rs new file mode 100644 index 000000000000..7a3ff958f6db --- /dev/null +++ b/crates/biome_grit_patterns/src/grit_js_parser.rs @@ -0,0 +1,53 @@ +use crate::{grit_analysis_ext::GritAnalysisExt, grit_tree::GritTree}; +use biome_js_parser::{parse, JsParserOptions}; +use biome_js_syntax::JsFileSource; +use grit_util::{AnalysisLogs, FileOrigin, Parser, SnippetTree}; +use std::path::Path; + +pub struct GritJsParser; + +impl Parser for GritJsParser { + type Tree = GritTree; + + fn parse_file( + &mut self, + body: &str, + path: Option<&Path>, + logs: &mut AnalysisLogs, + _old_tree: FileOrigin<'_, GritTree>, + ) -> Option { + let parse_result = parse(body, JsFileSource::tsx(), JsParserOptions::default()); + + for diagnostic in parse_result.diagnostics() { + logs.push(diagnostic.to_log(path)); + } + + Some(GritTree::new(parse_result.syntax().into())) + } + + fn parse_snippet( + &mut self, + prefix: &'static str, + source: &str, + postfix: &'static str, + ) -> SnippetTree { + let context = format!("{prefix}{source}{postfix}"); + + let len = if cfg!(target_arch = "wasm32") { + |src: &str| src.chars().count() as u32 + } else { + |src: &str| src.len() as u32 + }; + + let parse_result = parse(&context, JsFileSource::tsx(), JsParserOptions::default()); + + SnippetTree { + tree: GritTree::new(parse_result.syntax().into()), + source: source.to_owned(), + prefix, + postfix, + snippet_start: (len(prefix) + len(source) - len(source.trim_start())), + snippet_end: (len(prefix) + len(source.trim_end())), + } + } +} diff --git a/crates/biome_grit_patterns/src/grit_node.rs b/crates/biome_grit_patterns/src/grit_node.rs index 6577ce2184d7..656c3659778a 100644 --- a/crates/biome_grit_patterns/src/grit_node.rs +++ b/crates/biome_grit_patterns/src/grit_node.rs @@ -94,12 +94,6 @@ impl GritAstNode for GritNode { } } - fn full_source(&self) -> &str { - // This should not be a problem anytime soon, though we may want to - // reconsider when we implement rewrites. - unimplemented!("Full source of file not available") - } - fn walk(&self) -> impl AstCursor { GritNodeCursor::new(self) } diff --git a/crates/biome_grit_patterns/src/grit_node_patterns.rs b/crates/biome_grit_patterns/src/grit_node_patterns.rs index 8112b6059c8c..661974ef4db1 100644 --- a/crates/biome_grit_patterns/src/grit_node_patterns.rs +++ b/crates/biome_grit_patterns/src/grit_node_patterns.rs @@ -11,6 +11,8 @@ use grit_util::AnalysisLogs; pub(crate) struct GritNodePattern; impl AstNodePattern for GritNodePattern { + const INCLUDES_TRIVIA: bool = true; + fn children(&self) -> Vec> { todo!() } diff --git a/crates/biome_grit_patterns/src/grit_query.rs b/crates/biome_grit_patterns/src/grit_query.rs index ee925b0c4b8a..f43b55404d7c 100644 --- a/crates/biome_grit_patterns/src/grit_query.rs +++ b/crates/biome_grit_patterns/src/grit_query.rs @@ -10,7 +10,7 @@ use crate::variables::{VarRegistry, VariableLocations}; use crate::CompileError; use anyhow::Result; use biome_grit_syntax::{GritRoot, GritRootExt}; -use grit_pattern_matcher::pattern::{Matcher, Pattern, State}; +use grit_pattern_matcher::pattern::{FileRegistry, Matcher, Pattern, State}; use std::collections::BTreeMap; /// Represents a top-level Grit query. @@ -32,7 +32,10 @@ impl GritQuery { let binding = GritResolvedPattern; let context = GritExecContext; - let mut state = State::new(var_registry.into(), Vec::new()); + let mut state = State::new( + var_registry.into(), + FileRegistry::new_from_paths(Vec::new()), + ); let mut logs = Vec::new().into(); self.pattern diff --git a/crates/biome_grit_patterns/src/grit_target_language.rs b/crates/biome_grit_patterns/src/grit_target_language.rs index d070e60de5b2..27553cd03f0a 100644 --- a/crates/biome_grit_patterns/src/grit_target_language.rs +++ b/crates/biome_grit_patterns/src/grit_target_language.rs @@ -2,9 +2,12 @@ mod js_target_language; pub use js_target_language::JsTargetLanguage; +use crate::grit_js_parser::GritJsParser; use crate::grit_target_node::{GritTargetNode, GritTargetSyntaxKind}; +use crate::grit_tree::GritTree; use biome_rowan::SyntaxKind; -use grit_util::Language; +use grit_util::{Ast, CodeRange, EffectRange, Language, Parser, SnippetTree}; +use std::borrow::Cow; /// Generates the `GritTargetLanguage` enum. /// @@ -13,7 +16,7 @@ use grit_util::Language; /// implement the slightly more convenient [`GritTargetLanguageImpl`] for /// creating language-specific implementations. macro_rules! generate_target_language { - ($($language:ident),+) => { + ($([$language:ident, $parser:ident]),+) => { #[derive(Clone, Debug)] pub enum GritTargetLanguage { $($language($language)),+ @@ -32,11 +35,26 @@ macro_rules! generate_target_language { } } + fn get_parser(&self) -> Box> { + match self { + $(Self::$language(_) => Box::new($parser)),+ + } + } + fn is_alternative_metavariable_kind(&self, kind: GritTargetSyntaxKind) -> bool { match self { $(Self::$language(_) => $language::is_alternative_metavariable_kind(kind)),+ } } + + pub fn parse_snippet_contexts(&self, source: &str) -> Vec> { + let source = self.substitute_metavariable_prefix(source); + self.snippet_context_strings() + .iter() + .map(|(pre, post)| self.get_parser().parse_snippet(pre, &source, post)) + .filter(|result| !result.tree.root_node().kind().is_bogus()) + .collect() + } } impl Language for GritTargetLanguage { @@ -65,12 +83,32 @@ macro_rules! generate_target_language { || (self.is_alternative_metavariable_kind(node.kind()) && self.exact_replaced_variable_regex().is_match(&node.text_trimmed().to_string())) } + + fn align_padding<'a>( + &self, + _node: &Self::Node<'a>, + _range: &CodeRange, + _skip_ranges: &[CodeRange], + _new_padding: Option, + _offset: usize, + _substitutions: &mut [(EffectRange, String)], + ) -> Cow<'a, str> { + todo!() + } + + fn pad_snippet<'a>(&self, _snippet: &'a str, _padding: &str) -> Cow<'a, str> { + todo!() + } + + fn get_skip_padding_ranges(&self, _node: &Self::Node<'_>) -> Vec { + Vec::new() + } } - }; + } } generate_target_language! { - JsTargetLanguage + [JsTargetLanguage, GritJsParser] } /// Trait to be implemented by the language-specific implementations. diff --git a/crates/biome_grit_patterns/src/grit_target_node.rs b/crates/biome_grit_patterns/src/grit_target_node.rs index 7cdc02efdfb1..733d57e78db0 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::{SyntaxNodeText, TextRange}; +use biome_rowan::{SyntaxKind, SyntaxNodeText, TextRange}; use grit_util::{AstCursor, AstNode as GritAstNode, ByteRange, CodeRange}; use std::{borrow::Cow, str::Utf8Error}; @@ -73,6 +73,14 @@ macro_rules! generate_target_node { $(Self::$lang_node(node) => node.text_trimmed_range()),+ } } + + pub fn start_byte(&self) -> u32 { + self.text_trimmed_range().start().into() + } + + pub fn end_byte(&self) -> u32 { + self.text_trimmed_range().end().into() + } } impl GritAstNode for GritTargetNode { @@ -144,12 +152,6 @@ macro_rules! generate_target_node { } } - fn full_source(&self) -> &str { - // This should not be a problem anytime soon, though we may want to - // reconsider when we implement rewrites. - unimplemented!("Full source of file not available") - } - fn walk(&self) -> impl AstCursor { GritTargetNodeCursor::new(self) } @@ -184,6 +186,14 @@ macro_rules! generate_target_node { Self::$lang_kind(value) } })+ + + impl GritTargetSyntaxKind { + pub fn is_bogus(&self) -> bool { + match self { + $(Self::$lang_kind(kind) => kind.is_bogus()),+ + } + } + } }; } diff --git a/crates/biome_grit_patterns/src/grit_tree.rs b/crates/biome_grit_patterns/src/grit_tree.rs index 37852bc1fc99..368909d74cbf 100644 --- a/crates/biome_grit_patterns/src/grit_tree.rs +++ b/crates/biome_grit_patterns/src/grit_tree.rs @@ -1,11 +1,18 @@ use crate::grit_target_node::GritTargetNode; use grit_util::Ast; +use std::borrow::Cow; #[derive(Clone, Debug, PartialEq)] -pub(crate) struct GritTree { +pub struct GritTree { root: GritTargetNode, } +impl GritTree { + pub fn new(root: GritTargetNode) -> Self { + Self { root } + } +} + impl Ast for GritTree { type Node<'a> = GritTargetNode where @@ -14,4 +21,8 @@ impl Ast for GritTree { fn root_node(&self) -> GritTargetNode { self.root.clone() } + + fn source(&self) -> Cow { + self.root.text().to_string().into() + } } diff --git a/crates/biome_grit_patterns/src/lib.rs b/crates/biome_grit_patterns/src/lib.rs index e729bfe8aff1..c525ebc1e4bf 100644 --- a/crates/biome_grit_patterns/src/lib.rs +++ b/crates/biome_grit_patterns/src/lib.rs @@ -1,10 +1,12 @@ #![allow(dead_code)] // FIXME: Remove when more stuff is ready mod diagnostics; mod errors; +mod grit_analysis_ext; mod grit_binding; mod grit_code_snippet; mod grit_context; mod grit_file; +mod grit_js_parser; mod grit_node; mod grit_node_patterns; mod grit_query; diff --git a/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs b/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs index 28b00451ff98..428c6ef89f04 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs @@ -1,9 +1,10 @@ use super::{ compilation_context::NodeCompilationContext, list_compiler::ListCompiler, - map_compiler::MapCompiler, + map_compiler::MapCompiler, snippet_compiler::parse_snippet_content, }; -use crate::{grit_context::GritQueryContext, CompileError}; -use biome_grit_syntax::{AnyGritLiteral, GritSyntaxKind}; +use crate::{grit_context::GritQueryContext, util::TextRangeGritExt, CompileError}; +use biome_grit_syntax::{AnyGritCodeSnippetSource, AnyGritLiteral, GritSyntaxKind}; +use biome_rowan::AstNode; use grit_pattern_matcher::pattern::{ BooleanConstant, FloatConstant, IntConstant, Pattern, StringConstant, }; @@ -20,7 +21,16 @@ impl LiteralCompiler { AnyGritLiteral::GritBooleanLiteral(node) => Ok(Pattern::BooleanConstant( BooleanConstant::new(node.value()?.text_trimmed() == "true"), )), - AnyGritLiteral::GritCodeSnippet(_) => todo!(), + AnyGritLiteral::GritCodeSnippet(node) => match node.source()? { + AnyGritCodeSnippetSource::GritBacktickSnippetLiteral(node) => { + let token = node.value_token()?; + let text = token.text_trimmed(); + let range = node.syntax().text_trimmed_range().to_byte_range(); + parse_snippet_content(&text[1..text.len() - 1], range, context, is_rhs) + } + AnyGritCodeSnippetSource::GritLanguageSpecificSnippet(_) => todo!(), + AnyGritCodeSnippetSource::GritRawBacktickSnippetLiteral(_) => todo!(), + }, AnyGritLiteral::GritDoubleLiteral(node) => Ok(Pattern::FloatConstant( FloatConstant::new(node.value_token()?.text_trimmed().parse().map_err(|err| { CompileError::LiteralOutOfRange(format!("Error parsing double: {err}")) 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 1705fd792198..a166161a2719 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs @@ -1,6 +1,191 @@ -use grit_util::{ByteRange, Language}; +use super::compilation_context::NodeCompilationContext; +use crate::{ + grit_code_snippet::GritCodeSnippet, grit_context::GritQueryContext, + grit_target_node::GritTargetNode, grit_tree::GritTree, CompileError, +}; +use grit_pattern_matcher::{ + constants::GLOBAL_VARS_SCOPE_INDEX, + pattern::{DynamicPattern, DynamicSnippet, DynamicSnippetPart, Pattern, Variable}, +}; +use grit_util::{Ast, AstNode, ByteRange, Language, SnippetTree}; use std::borrow::Cow; +pub(crate) fn parse_snippet_content( + source: &str, + range: ByteRange, + context: &mut NodeCompilationContext, + is_rhs: bool, +) -> Result, CompileError> { + // we check for BRACKET_VAR_REGEX in the content, and if found + // compile into a DynamicPattern, rather than a CodeSnippet. + // This is because the syntax should only ever be necessary + // when treating a metavariable as a string to substitute + // rather than an AST node to match on. eg. in the following + // `const ${name}Handler = useCallback(async () => $body, []);` + // $name does not correspond to a node, but rather prepends a + // string to "Handler", which will together combine into an + // identifier. + if context + .compilation + .lang + .metavariable_bracket_regex() + .is_match(source) + { + return if is_rhs { + Ok(Pattern::Dynamic( + dynamic_snippet_from_source(source, range, context).map(DynamicPattern::Snippet)?, + )) + } else { + Err(CompileError::InvalidBracketedMetavariable) + }; + } + + if context + .compilation + .lang + .exact_variable_regex() + .is_match(source.trim()) + { + return match source.trim() { + "$_" => Ok(Pattern::Underscore), + "^_" => Ok(Pattern::Underscore), + name => { + let var = context.register_variable(name.to_owned(), range)?; + Ok(Pattern::Variable(var)) + } + }; + } + + let snippet_trees = context.compilation.lang.parse_snippet_contexts(source); + let snippet_nodes = nodes_from_indices(&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 + // to parse. unfortunately got rid of check to support + // passing non-node snippets as args. + return Ok(Pattern::Dynamic( + dynamic_snippet_from_source(source, range, context).map(DynamicPattern::Snippet)?, + )); + } + + let dynamic_snippet = dynamic_snippet_from_source(source, range, context) + .map_or(None, |s| Some(DynamicPattern::Snippet(s))); + Ok(Pattern::CodeSnippet(GritCodeSnippet { + dynamic_snippet, + source: source.to_owned(), + })) +} + +pub(crate) fn dynamic_snippet_from_source( + raw_source: &str, + source_range: ByteRange, + context: &mut NodeCompilationContext, +) -> Result { + let source_string = raw_source + .replace("\\n", "\n") + .replace("\\$", "$") + .replace("\\^", "^") + .replace("\\`", "`") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + let source = source_string.as_str(); + let metavariables = split_snippet(source, &context.compilation.lang); + let mut parts = Vec::with_capacity(2 * metavariables.len() + 1); + let mut last = 0; + // Reverse the iterator so we go over the variables in ascending order. + for (byte_range, var) in metavariables.into_iter().rev() { + parts.push(DynamicSnippetPart::String( + source[last..byte_range.start].to_string(), + )); + let range = ByteRange::new( + source_range.start + byte_range.start, + source_range.start + byte_range.start + var.len(), + ); + if let Some(var) = context.vars.get(var.as_ref()) { + context.vars_array[context.scope_index][*var] + .locations + .insert(range); + parts.push(DynamicSnippetPart::Variable(Variable::new( + context.scope_index, + *var, + ))); + } else if let Some(var) = context.global_vars.get(var.as_ref()) { + parts.push(DynamicSnippetPart::Variable(Variable::new( + GLOBAL_VARS_SCOPE_INDEX, + *var, + ))); + } else if var.starts_with("$GLOBAL_") { + let variable = context.register_variable(var.to_string(), range)?; + parts.push(DynamicSnippetPart::Variable(variable)); + } else { + return Err(CompileError::UnknownVariable(var.to_string())); + } + last = byte_range.end; + } + parts.push(DynamicSnippetPart::String(source[last..].to_string())); + + Ok(DynamicSnippet { parts }) +} + +pub fn nodes_from_indices(indices: &[SnippetTree]) -> Vec { + indices + .iter() + .filter_map(snippet_nodes_from_index) + .collect() +} + +fn snippet_nodes_from_index(snippet: &SnippetTree) -> Option { + let mut snippet_root = snippet.tree.root_node(); + + // find the outermost node with the same index as the snippet + 'outer: while snippet_root.start_byte() < snippet.snippet_start + || snippet_root.end_byte() > snippet.snippet_end + { + let mut has_children = false; + for child in snippet_root.clone().children() { + has_children = true; + + if child.start_byte() <= snippet.snippet_start + && child.end_byte() >= snippet.snippet_end + { + snippet_root = child; + continue 'outer; + } + } + + if snippet_root.text() != snippet.source.trim() { + return None; + } + + if !has_children { + return Some(snippet_root); + } + + break; + } + + // in order to handle white space and other superfluous + // stuff in the snippet we assume the root + // is correct as long as it's the largest node within + // the snippet length. Maybe this is too permissive? + let mut nodes = Vec::new(); + let root_start = snippet_root.start_byte(); + let root_end = snippet_root.end_byte(); + if root_start > snippet.snippet_start || root_end < snippet.snippet_end { + return None; + } + while snippet_root.start_byte() == root_start && snippet_root.end_byte() == root_end { + let first_child = snippet_root.children().next(); + nodes.push(snippet_root); + if let Some(child) = first_child { + snippet_root = child + } else { + break; + } + } + nodes.last().cloned() +} + /// Takes a snippet with metavariables and returns a list of ranges and the /// corresponding metavariables. /// diff --git a/crates/biome_grit_patterns/tests/quick_test.rs b/crates/biome_grit_patterns/tests/quick_test.rs new file mode 100644 index 000000000000..6b515c523141 --- /dev/null +++ b/crates/biome_grit_patterns/tests/quick_test.rs @@ -0,0 +1,23 @@ +use biome_grit_parser::parse_grit; +use biome_grit_patterns::{GritQuery, GritTargetLanguage, JsTargetLanguage}; + +// 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() { + println!( + "Diagnostics from parsing query:\n{:?}", + parse_result.diagnostics() + ); + } + + let query = GritQuery::from_node( + parse_result.tree(), + GritTargetLanguage::JsTargetLanguage(JsTargetLanguage), + ) + .expect("could not construct query"); + + query.execute().expect("could not execute query"); +} From 47e2cd8d49882b7e713c8bd993c61416f3a62230 Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Tue, 4 Jun 2024 19:34:56 +0200 Subject: [PATCH 2/2] PR feedback --- crates/biome_diagnostics/src/display.rs | 2 +- .../src/grit_analysis_ext.rs | 46 +++++-------------- crates/biome_grit_patterns/src/lib.rs | 1 + .../src/pattern_compiler/literal_compiler.rs | 1 + .../src/source_location_ext.rs | 34 ++++++++++++++ 5 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 crates/biome_grit_patterns/src/source_location_ext.rs diff --git a/crates/biome_diagnostics/src/display.rs b/crates/biome_diagnostics/src/display.rs index 53415832a3d4..c6854bda82c1 100644 --- a/crates/biome_diagnostics/src/display.rs +++ b/crates/biome_diagnostics/src/display.rs @@ -10,7 +10,7 @@ mod diff; pub(super) mod frame; mod message; -pub use crate::display::frame::SourceFile; +pub use crate::display::frame::{SourceFile, SourceLocation}; use crate::{ diagnostic::internal::AsDiagnostic, Advices, Diagnostic, DiagnosticTags, Location, LogCategory, Resource, Severity, Visit, diff --git a/crates/biome_grit_patterns/src/grit_analysis_ext.rs b/crates/biome_grit_patterns/src/grit_analysis_ext.rs index f51f6459b5b5..f60c4fb8e11e 100644 --- a/crates/biome_grit_patterns/src/grit_analysis_ext.rs +++ b/crates/biome_grit_patterns/src/grit_analysis_ext.rs @@ -1,7 +1,9 @@ -use biome_diagnostics::{Diagnostic, PrintDescription, Severity}; -use grit_util::{AnalysisLog, Position, Range}; +use biome_diagnostics::{display::SourceFile, Diagnostic, PrintDescription, Severity}; +use grit_util::AnalysisLog; use std::path::{Path, PathBuf}; +use crate::source_location_ext::SourceFileExt; + pub trait GritAnalysisExt { fn to_log(&self, path: Option<&Path>) -> AnalysisLog; } @@ -12,38 +14,12 @@ where { fn to_log(&self, path: Option<&Path>) -> AnalysisLog { let location = self.location(); - let source = location.source_code; - let range = - match (location.span, source) { - (Some(range), Some(source)) => source.text[..range.start().into()] - .lines() - .enumerate() - .last() - .map(|(i, line)| { - let start = Position { - line: (i + 1) as u32, - column: line.len() as u32, - }; - let end = source.text[range].lines().enumerate().last().map_or( - start, - |(j, line)| Position { - line: start.line + j as u32, - column: if j == 0 { - start.column + line.len() as u32 - } else { - line.len() as u32 - }, - }, - ); - Range { - start, - end, - start_byte: range.start().into(), - end_byte: range.end().into(), - } - }), - _ => None, - }; + let source = location.source_code.map(SourceFile::new); + + let range = match (location.span, source) { + (Some(range), Some(source)) => source.to_grit_range(range), + _ => None, + }; AnalysisLog { engine_id: Some("biome".to_owned()), @@ -63,7 +39,7 @@ where position: range.as_ref().map(|r| r.start), range, syntax_tree: None, - source: source.map(|s| s.text.to_owned()), + source: location.source_code.map(|s| s.text.to_string()), } } } diff --git a/crates/biome_grit_patterns/src/lib.rs b/crates/biome_grit_patterns/src/lib.rs index c525ebc1e4bf..40b8d76ed8a8 100644 --- a/crates/biome_grit_patterns/src/lib.rs +++ b/crates/biome_grit_patterns/src/lib.rs @@ -15,6 +15,7 @@ mod grit_target_node; mod grit_tree; mod pattern_compiler; mod resolved_pattern; +mod source_location_ext; mod util; mod variables; diff --git a/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs b/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs index 428c6ef89f04..b6c82e04243c 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/literal_compiler.rs @@ -26,6 +26,7 @@ impl LiteralCompiler { let token = node.value_token()?; let text = token.text_trimmed(); let range = node.syntax().text_trimmed_range().to_byte_range(); + debug_assert!(text.len() >= 2, "Literals must have quotes"); parse_snippet_content(&text[1..text.len() - 1], range, context, is_rhs) } AnyGritCodeSnippetSource::GritLanguageSpecificSnippet(_) => todo!(), diff --git a/crates/biome_grit_patterns/src/source_location_ext.rs b/crates/biome_grit_patterns/src/source_location_ext.rs new file mode 100644 index 000000000000..bc985fe6c820 --- /dev/null +++ b/crates/biome_grit_patterns/src/source_location_ext.rs @@ -0,0 +1,34 @@ +use biome_diagnostics::display::{SourceFile, SourceLocation}; +use biome_rowan::TextRange; +use grit_util::{Position, Range}; + +pub trait SourceFileExt { + fn to_grit_range(&self, range: TextRange) -> Option; +} + +impl<'d> SourceFileExt for SourceFile<'d> { + fn to_grit_range(&self, range: TextRange) -> Option { + let start = self.location(range.start()).ok()?; + let end = self.location(range.end()).ok()?; + + Some(Range { + start: start.to_grit_position(), + end: end.to_grit_position(), + start_byte: range.start().into(), + end_byte: range.end().into(), + }) + } +} + +pub trait SourceLocationExt { + fn to_grit_position(&self) -> Position; +} + +impl SourceLocationExt for SourceLocation { + fn to_grit_position(&self) -> Position { + Position { + column: self.column_number.get() as u32, + line: self.line_number.get() as u32, + } + } +}