From 38159059085db34fe2f34c2ad65f87f9377f59dc Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Thu, 19 Sep 2024 13:04:17 +0200 Subject: [PATCH] feat(grit): implement Grit built-ins (#3987) --- Cargo.lock | 1 + crates/biome_grit_patterns/Cargo.toml | 1 + crates/biome_grit_patterns/src/errors.rs | 11 + .../src/grit_built_in_functions.rs | 400 ++++++++++++++++++ .../biome_grit_patterns/src/grit_context.rs | 50 +-- crates/biome_grit_patterns/src/grit_query.rs | 24 +- crates/biome_grit_patterns/src/lib.rs | 1 + .../src/pattern_compiler/call_compiler.rs | 50 ++- .../pattern_compiler/compilation_context.rs | 13 +- .../pattern_compiler/node_like_compiler.rs | 2 +- .../src/pattern_compiler/snippet_compiler.rs | 20 +- .../tests/specs/ts/capitalize.grit | 3 + .../tests/specs/ts/capitalize.snap | 12 + .../tests/specs/ts/capitalize.ts | 1 + 14 files changed, 534 insertions(+), 55 deletions(-) create mode 100644 crates/biome_grit_patterns/src/grit_built_in_functions.rs create mode 100644 crates/biome_grit_patterns/tests/specs/ts/capitalize.grit create mode 100644 crates/biome_grit_patterns/tests/specs/ts/capitalize.snap create mode 100644 crates/biome_grit_patterns/tests/specs/ts/capitalize.ts diff --git a/Cargo.lock b/Cargo.lock index ff076684f598..69175e8fee25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -639,6 +639,7 @@ dependencies = [ "im", "insta", "path-absolutize", + "rand 0.8.5", "regex", "rustc-hash 1.1.0", "serde", diff --git a/crates/biome_grit_patterns/Cargo.toml b/crates/biome_grit_patterns/Cargo.toml index b15b9d47d433..9938a2bb80ea 100644 --- a/crates/biome_grit_patterns/Cargo.toml +++ b/crates/biome_grit_patterns/Cargo.toml @@ -25,6 +25,7 @@ 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"] } +rand = { version = "0.8" } 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 501152a7d52e..8ba3ee25283d 100644 --- a/crates/biome_grit_patterns/src/errors.rs +++ b/crates/biome_grit_patterns/src/errors.rs @@ -15,6 +15,9 @@ pub enum CompileError { /// Used for missing syntax nodes. MissingSyntaxNode, + /// A built-in function call was discovered in an unexpected context. + UnexpectedBuiltinCall(String), + /// A metavariables was discovered in an unexpected context. UnexpectedMetavariable, @@ -95,6 +98,9 @@ impl Diagnostic for CompileError { CompileError::MissingSyntaxNode => { fmt.write_markup(markup! { "A syntax node was missing" }) } + CompileError::UnexpectedBuiltinCall(name) => { + fmt.write_markup(markup! { "Unexpected call to built-in: "{{name}}"()" }) + } CompileError::UnexpectedMetavariable => { fmt.write_markup(markup! { "Unexpected metavariable" }) } @@ -176,6 +182,11 @@ impl Diagnostic for CompileError { fn advices(&self, visitor: &mut dyn biome_diagnostics::Visit) -> std::io::Result<()> { match self { + CompileError::UnexpectedBuiltinCall(name) => visitor.record_log( + LogCategory::Info, + &markup! { "Built-in "{{name}}" can only be used on the right-hand side of a rewrite" } + .to_owned(), + ), CompileError::ReservedMetavariable(_) => visitor.record_log( LogCategory::Info, &markup! { "Try using a different variable name" }.to_owned(), diff --git a/crates/biome_grit_patterns/src/grit_built_in_functions.rs b/crates/biome_grit_patterns/src/grit_built_in_functions.rs new file mode 100644 index 000000000000..108c356f5118 --- /dev/null +++ b/crates/biome_grit_patterns/src/grit_built_in_functions.rs @@ -0,0 +1,400 @@ +use crate::{ + grit_context::{GritExecContext, GritQueryContext}, + grit_resolved_pattern::GritResolvedPattern, +}; +use anyhow::{anyhow, bail, Result}; +use grit_pattern_matcher::{ + binding::Binding, + constant::Constant, + context::ExecContext, + pattern::{ + get_absolute_file_name, CallBuiltIn, JoinFn, LazyBuiltIn, Pattern, ResolvedPattern, + ResolvedSnippet, State, + }, +}; +use grit_util::AnalysisLogs; +use im::Vector; +use path_absolutize::Absolutize; +use rand::{seq::SliceRandom, Rng}; +use std::borrow::Cow; +use std::path::Path; + +pub type CallableFn = dyn for<'a> Fn( + &'a [Option>], + &'a GritExecContext<'a>, + &mut State<'a, GritQueryContext>, + &mut AnalysisLogs, + ) -> Result> + + Send + + Sync; + +pub struct BuiltInFunction { + pub name: &'static str, + pub params: Vec<&'static str>, + pub(crate) func: Box, +} + +impl BuiltInFunction { + fn call<'a>( + &self, + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, + ) -> Result> { + (self.func)(args, context, state, logs) + } + + pub fn new(name: &'static str, params: Vec<&'static str>, func: Box) -> Self { + Self { name, params, func } + } +} + +impl std::fmt::Debug for BuiltInFunction { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("BuiltInFunction") + .field("name", &self.name) + .field("params", &self.params) + .finish() + } +} + +#[derive(Debug)] +pub struct BuiltIns(Vec); + +impl Default for BuiltIns { + fn default() -> Self { + vec![ + BuiltInFunction::new("resolve", vec!["path"], Box::new(resolve_path_fn)), + BuiltInFunction::new("capitalize", vec!["string"], Box::new(capitalize_fn)), + BuiltInFunction::new("lowercase", vec!["string"], Box::new(lowercase_fn)), + BuiltInFunction::new("uppercase", vec!["string"], Box::new(uppercase_fn)), + BuiltInFunction::new("text", vec!["string"], Box::new(text_fn)), + BuiltInFunction::new("trim", vec!["string", "trim_chars"], Box::new(trim_fn)), + BuiltInFunction::new("join", vec!["list", "separator"], Box::new(join_fn)), + BuiltInFunction::new("distinct", vec!["list"], Box::new(distinct_fn)), + BuiltInFunction::new("length", vec!["target"], Box::new(length_fn)), + BuiltInFunction::new("shuffle", vec!["list"], Box::new(shuffle_fn)), + BuiltInFunction::new("random", vec!["floor", "ceiling"], Box::new(random_fn)), + BuiltInFunction::new("split", vec!["string", "separator"], Box::new(split_fn)), + ] + .into() + } +} + +impl BuiltIns { + pub(crate) fn call<'a>( + &self, + call: &'a CallBuiltIn, + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, + ) -> Result> { + self.0[call.index].call(&call.args, context, state, logs) + } + + pub(crate) fn get_built_ins(&self) -> &[BuiltInFunction] { + &self.0 + } +} + +impl From> for BuiltIns { + fn from(built_ins: Vec) -> Self { + Self(built_ins) + } +} + +fn capitalize_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("capitalize() takes 1 argument: string"); + }; + + let string = arg1.text(&state.files, context.language())?; + Ok(ResolvedPattern::from_string(capitalize(&string))) +} + +fn distinct_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("distinct() takes 1 argument: list"); + }; + + match arg1 { + GritResolvedPattern::List(list) => { + let mut unique_list = Vector::new(); + for item in list { + if !unique_list.contains(&item) { + unique_list.push_back(item); + } + } + Ok(GritResolvedPattern::List(unique_list)) + } + GritResolvedPattern::Binding(binding) => match binding.last() { + Some(binding) => { + let Some(list_items) = binding.list_items() else { + bail!("distinct() requires a list as the first argument"); + }; + + let mut unique_list = Vector::new(); + for item in list_items { + let resolved = ResolvedPattern::from_node_binding(item); + if !unique_list.contains(&resolved) { + unique_list.push_back(resolved); + } + } + Ok(GritResolvedPattern::List(unique_list)) + } + None => Ok(GritResolvedPattern::Binding(binding)), + }, + _ => bail!("distinct() requires a list as the first argument"), + } +} + +fn join_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let mut args = args.into_iter(); + let (Some(Some(arg1)), Some(Some(arg2))) = (args.next(), args.next()) else { + bail!("join() takes 2 arguments: list and separator"); + }; + + let separator = arg2.text(&state.files, context.language())?; + let join = if let Some(items) = arg1.get_list_items() { + JoinFn::from_patterns(items.cloned(), separator.to_string()) + } else if let Some(items) = arg1.get_list_binding_items() { + JoinFn::from_patterns(items, separator.to_string()) + } else { + bail!("join() requires a list as the first argument"); + }; + + let snippet = ResolvedSnippet::LazyFn(Box::new(LazyBuiltIn::Join(join))); + Ok(ResolvedPattern::from_resolved_snippet(snippet)) +} + +fn length_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("length() takes 1 argument: list or string"); + }; + + Ok(match arg1 { + GritResolvedPattern::List(list) => { + ResolvedPattern::from_constant(Constant::Integer(list.len().try_into()?)) + } + GritResolvedPattern::Binding(binding) => match binding.last() { + Some(resolved_pattern) => { + let length = if let Some(list_items) = resolved_pattern.list_items() { + list_items.count() + } else { + resolved_pattern.text(context.language())?.len() + }; + ResolvedPattern::from_constant(Constant::Integer(length.try_into()?)) + } + None => bail!("length() requires a list or string as the first argument"), + }, + resolved_pattern => { + let Ok(text) = resolved_pattern.text(&state.files, context.language()) else { + bail!("length() requires a list or string as the first argument"); + }; + + ResolvedPattern::from_constant(Constant::Integer(text.len().try_into()?)) + } + }) +} + +fn lowercase_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("lowercase() takes 1 argument: string"); + }; + + let string = arg1.text(&state.files, context.language())?; + Ok(ResolvedPattern::from_string(string.to_lowercase())) +} + +fn random_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + + match args.as_slice() { + [Some(start), Some(end)] => { + let start = start.text(&state.files, context.language())?; + let end = end.text(&state.files, context.language())?; + let start = start.parse::()?; + let end = end.parse::()?; + // Inclusive range + let value = state.get_rng().gen_range(start..=end); + Ok(ResolvedPattern::from_constant(Constant::Integer(value))) + } + [None, None] => { + let value = state.get_rng().gen::(); + Ok(ResolvedPattern::from_constant(Constant::Float(value))) + } + _ => bail!("random() takes 0 or 2 arguments: an optional start and end"), + } +} + +/// Turns an arbitrary path into a resolved and normalized absolute path +fn resolve_path_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("resolve() takes 1 argument: path"); + }; + + let current_file = get_absolute_file_name(state, context.language())?; + let target_path = arg1.text(&state.files, context.language())?; + + let resolved_path = resolve(target_path, current_file.into())?; + + Ok(ResolvedPattern::from_string(resolved_path)) +} + +fn shuffle_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("shuffle() takes 1 argument: list"); + }; + + let mut list: Vec<_> = if let Some(items) = arg1.get_list_items() { + items.cloned().collect() + } else if let Some(items) = arg1.get_list_binding_items() { + items.collect() + } else { + bail!("shuffle() requires a list as the first argument"); + }; + + list.shuffle(state.get_rng()); + Ok(GritResolvedPattern::from_list_parts(list.into_iter())) +} + +fn split_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let mut args = args.into_iter(); + let (Some(Some(arg1)), Some(Some(arg2))) = (args.next(), args.next()) else { + bail!("split() takes 2 arguments: string and separator"); + }; + + let separator = arg2.text(&state.files, context.language())?; + let separator = separator.as_ref(); + + let string = arg1.text(&state.files, context.language())?; + let parts = string.split(separator).map(|s| { + ResolvedPattern::from_resolved_snippet(ResolvedSnippet::Text(s.to_string().into())) + }); + Ok(ResolvedPattern::from_list_parts(parts)) +} + +fn text_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("text() takes 1 argument: string"); + }; + + let string = arg1.text(&state.files, context.language())?; + Ok(ResolvedPattern::from_string(string.to_string())) +} + +fn trim_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let mut args = args.into_iter(); + let (Some(Some(arg1)), Some(Some(arg2))) = (args.next(), args.next()) else { + bail!("trim() takes 2 arguments: string and trim_chars"); + }; + + let trim_chars = arg2.text(&state.files, context.language())?; + let trim_chars: Vec = trim_chars.chars().collect(); + let trim_chars = trim_chars.as_slice(); + + let string = arg1.text(&state.files, context.language())?; + let string = string.trim_matches(trim_chars).to_string(); + Ok(ResolvedPattern::from_string(string)) +} + +fn uppercase_fn<'a>( + args: &'a [Option>], + context: &'a GritExecContext<'a>, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, +) -> Result> { + let args = GritResolvedPattern::from_patterns(args, state, context, logs)?; + let Some(Some(arg1)) = args.into_iter().next() else { + bail!("uppercase() takes 1 argument: string"); + }; + + let string = arg1.text(&state.files, context.language())?; + Ok(ResolvedPattern::from_string(string.to_uppercase())) +} + +fn capitalize(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().collect::() + chars.as_str(), + } +} + +fn resolve<'a>(target_path: Cow<'a, str>, from_file: Cow<'a, str>) -> Result { + let Some(source_path) = Path::new(from_file.as_ref()).parent() else { + bail!("could not get parent directory of file name {}", &from_file); + }; + let our_path = Path::new(target_path.as_ref()); + let absolutized = our_path.absolutize_from(source_path)?; + Ok(absolutized + .to_str() + .ok_or_else(|| anyhow!("could not build absolute path from file name {target_path}"))? + .to_owned()) +} diff --git a/crates/biome_grit_patterns/src/grit_context.rs b/crates/biome_grit_patterns/src/grit_context.rs index 33883b7fed4c..5d29a8f1d226 100644 --- a/crates/biome_grit_patterns/src/grit_context.rs +++ b/crates/biome_grit_patterns/src/grit_context.rs @@ -1,4 +1,5 @@ use crate::grit_binding::GritBinding; +use crate::grit_built_in_functions::BuiltIns; use crate::grit_code_snippet::GritCodeSnippet; use crate::grit_file::GritFile; use crate::grit_node_patterns::{GritLeafNodePattern, GritNodePattern}; @@ -38,38 +39,17 @@ impl QueryContext for GritQueryContext { pub struct GritExecContext<'a> { /// The language to which the snippet should apply. - lang: GritTargetLanguage, + pub lang: GritTargetLanguage, /// The name of the snippet being executed. - name: Option<&'a str>, - - loadable_files: &'a [GritTargetFile], - files: &'a FileOwners, - functions: &'a [GritFunctionDefinition], - patterns: &'a [PatternDefinition], - predicates: &'a [PredicateDefinition], -} - -impl<'a> GritExecContext<'a> { - pub fn new( - lang: GritTargetLanguage, - name: Option<&'a str>, - loadable_files: &'a [GritTargetFile], - files: &'a FileOwners, - functions: &'a [GritFunctionDefinition], - patterns: &'a [PatternDefinition], - predicates: &'a [PredicateDefinition], - ) -> Self { - Self { - lang, - name, - loadable_files, - files, - functions, - patterns, - predicates, - } - } + pub name: Option<&'a str>, + + pub loadable_files: &'a [GritTargetFile], + pub files: &'a FileOwners, + pub built_ins: &'a BuiltIns, + pub functions: &'a [GritFunctionDefinition], + pub patterns: &'a [PatternDefinition], + pub predicates: &'a [PredicateDefinition], } impl<'a> ExecContext<'a, GritQueryContext> for GritExecContext<'a> { @@ -91,12 +71,12 @@ impl<'a> ExecContext<'a, GritQueryContext> for GritExecContext<'a> { fn call_built_in( &self, - _call: &'a CallBuiltIn, - _context: &'a Self, - _state: &mut State<'a, GritQueryContext>, - _logs: &mut AnalysisLogs, + call: &'a CallBuiltIn, + context: &'a Self, + state: &mut State<'a, GritQueryContext>, + logs: &mut AnalysisLogs, ) -> Result> { - unimplemented!("built-in functions are still TODO") + self.built_ins.call(call, context, state, logs) } fn files(&self) -> &FileOwners { diff --git a/crates/biome_grit_patterns/src/grit_query.rs b/crates/biome_grit_patterns/src/grit_query.rs index 11edc833bfdf..63e33cb33db3 100644 --- a/crates/biome_grit_patterns/src/grit_query.rs +++ b/crates/biome_grit_patterns/src/grit_query.rs @@ -1,4 +1,5 @@ use crate::diagnostics::CompilerDiagnostic; +use crate::grit_built_in_functions::BuiltIns; use crate::grit_context::{GritExecContext, GritQueryContext, GritTargetFile}; use crate::grit_definitions::{ compile_definitions, scan_definitions, Definitions, ScannedDefinitionInfo, @@ -27,6 +28,9 @@ use im::Vector; use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; +use std::sync::LazyLock; + +static BUILT_INS: LazyLock = LazyLock::new(BuiltIns::default); // These need to remain ordered by index. const GLOBAL_VARS: [(&str, usize); 4] = [ @@ -64,15 +68,16 @@ impl GritQuery { let file_owners = FileOwners::new(); let files = vec![file]; let file_ptr = FilePtr::new(0, 0); - let context = GritExecContext::new( - self.language.clone(), - self.name.as_deref(), - &files, - &file_owners, - &self.definitions.functions, - &self.definitions.patterns, - &self.definitions.predicates, - ); + let context = GritExecContext { + lang: self.language.clone(), + name: self.name.as_deref(), + loadable_files: &files, + files: &file_owners, + built_ins: &BUILT_INS, + functions: &self.definitions.functions, + patterns: &self.definitions.patterns, + predicates: &self.definitions.predicates, + }; let var_registry = VarRegistry::from_locations(&self.variable_locations); @@ -112,6 +117,7 @@ impl GritQuery { let context = CompilationContext { source_path, lang, + built_ins: &BUILT_INS, pattern_definition_info, predicate_definition_info, function_definition_info, diff --git a/crates/biome_grit_patterns/src/lib.rs b/crates/biome_grit_patterns/src/lib.rs index f2ad2ea075b2..4f0c93a05e79 100644 --- a/crates/biome_grit_patterns/src/lib.rs +++ b/crates/biome_grit_patterns/src/lib.rs @@ -2,6 +2,7 @@ mod diagnostics; mod errors; mod grit_analysis_ext; mod grit_binding; +mod grit_built_in_functions; mod grit_code_snippet; mod grit_context; mod grit_definitions; diff --git a/crates/biome_grit_patterns/src/pattern_compiler/call_compiler.rs b/crates/biome_grit_patterns/src/pattern_compiler/call_compiler.rs index 6dc7bdd6caa2..79a6b2d52fe2 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/call_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/call_compiler.rs @@ -4,7 +4,7 @@ use biome_grit_syntax::{ AnyGritMaybeNamedArg, AnyGritPattern, GritNamedArgList, GritNodeLike, GritSyntaxKind, }; use biome_rowan::AstNode; -use grit_pattern_matcher::pattern::{Call, CallFunction, FilePattern, Pattern}; +use grit_pattern_matcher::pattern::{Call, CallBuiltIn, CallFunction, FilePattern, Pattern}; use grit_util::{ByteRange, Language}; use std::collections::BTreeMap; @@ -16,6 +16,7 @@ pub(super) fn call_pattern_from_node_with_name( node: &GritNodeLike, name: String, context: &mut NodeCompilationContext, + is_rhs: bool, ) -> Result, CompileError> { let named_args = named_args_from_node(node, &name, context)?; let mut args = named_args_to_map(named_args, context)?; @@ -45,6 +46,22 @@ pub(super) fn call_pattern_from_node_with_name( .map_or(Pattern::Underscore, |p| p.1); let body = args.remove_entry("$body").map_or(Pattern::Top, |p| p.1); Ok(Pattern::File(Box::new(FilePattern::new(name, body)))) + } else if let Some((index, built_in)) = context + .compilation + .built_ins + .get_built_ins() + .iter() + .enumerate() + .find(|(_, built_in)| built_in.name == name) + { + if !is_rhs { + return Err(CompileError::UnexpectedBuiltinCall(name)); + } + + let params = &built_in.params; + Ok(Pattern::CallBuiltIn(Box::new(call_built_in_from_args( + args, params, index, lang, + )?))) } else if let Some(info) = context.compilation.function_definition_info.get(&name) { let args = match_args_to_params(&name, args, &collect_params(&info.parameters), lang)?; Ok(Pattern::CallFunction(Box::new(CallFunction::new( @@ -58,6 +75,22 @@ pub(super) fn call_pattern_from_node_with_name( } } +fn call_built_in_from_args( + mut args: BTreeMap>, + params: &[&str], + index: usize, + lang: &impl Language, +) -> Result, CompileError> { + let mut pattern_params = Vec::with_capacity(args.len()); + for param in params.iter() { + match args.remove(&(lang.metavariable_prefix().to_owned() + param)) { + Some(p) => pattern_params.push(Some(p)), + None => pattern_params.push(None), + } + } + Ok(CallBuiltIn::new(index, pattern_params)) +} + pub(super) fn collect_params(parameters: &[(String, ByteRange)]) -> Vec { parameters.iter().map(|p| p.0.clone()).collect() } @@ -90,8 +123,21 @@ pub(super) fn named_args_from_node( name: &str, context: &mut NodeCompilationContext, ) -> Result, CompileError> { - let expected_params = if let Some(info) = context.compilation.function_definition_info.get(name) + let expected_params = if let Some(built_in) = context + .compilation + .built_ins + .get_built_ins() + .iter() + .find(|built_in| built_in.name == name) { + Some( + built_in + .params + .iter() + .map(|param| (*param).to_string()) + .collect(), + ) + } else if let Some(info) = context.compilation.function_definition_info.get(name) { Some(collect_params(&info.parameters)) } else { context diff --git a/crates/biome_grit_patterns/src/pattern_compiler/compilation_context.rs b/crates/biome_grit_patterns/src/pattern_compiler/compilation_context.rs index f0c9d112bc23..694809e0514e 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/compilation_context.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/compilation_context.rs @@ -1,7 +1,10 @@ use grit_pattern_matcher::pattern::VariableSourceLocations; use grit_util::ByteRange; -use crate::{diagnostics::CompilerDiagnostic, grit_target_language::GritTargetLanguage}; +use crate::{ + diagnostics::CompilerDiagnostic, grit_built_in_functions::BuiltIns, + grit_target_language::GritTargetLanguage, +}; use std::{collections::BTreeMap, path::Path}; pub(crate) struct CompilationContext<'a> { @@ -11,6 +14,7 @@ pub(crate) struct CompilationContext<'a> { /// The target language being matched on. pub lang: GritTargetLanguage, + pub built_ins: &'a BuiltIns, pub pattern_definition_info: BTreeMap, pub predicate_definition_info: BTreeMap, pub function_definition_info: BTreeMap, @@ -18,10 +22,15 @@ pub(crate) struct CompilationContext<'a> { impl<'a> CompilationContext<'a> { #[cfg(test)] - pub(crate) fn new(source_path: Option<&'a Path>, lang: GritTargetLanguage) -> Self { + pub(crate) fn new( + source_path: Option<&'a Path>, + lang: GritTargetLanguage, + built_ins: &'a BuiltIns, + ) -> Self { Self { source_path, lang, + built_ins, pattern_definition_info: Default::default(), predicate_definition_info: Default::default(), function_definition_info: Default::default(), diff --git a/crates/biome_grit_patterns/src/pattern_compiler/node_like_compiler.rs b/crates/biome_grit_patterns/src/pattern_compiler/node_like_compiler.rs index 7e405cf59e2a..625207d97eba 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/node_like_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/node_like_compiler.rs @@ -24,7 +24,7 @@ impl NodeLikeCompiler { if let Some(kind) = lang.kind_by_name(&name) { node_pattern_from_node_with_name_and_kind(node, name, kind, context, is_rhs) } else { - call_pattern_from_node_with_name(node, name, context) + call_pattern_from_node_with_name(node, name, context, is_rhs) } } } 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 08dd938bfea3..820c19fe56e8 100644 --- a/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs +++ b/crates/biome_grit_patterns/src/pattern_compiler/snippet_compiler.rs @@ -489,8 +489,8 @@ fn unescape(raw_string: &str) -> String { mod tests { use super::*; use crate::{ - grit_js_parser::GritJsParser, pattern_compiler::compilation_context::CompilationContext, - JsTargetLanguage, + grit_built_in_functions::BuiltIns, grit_js_parser::GritJsParser, + pattern_compiler::compilation_context::CompilationContext, JsTargetLanguage, }; use grit_util::Parser; use regex::Regex; @@ -561,8 +561,12 @@ mod tests { #[test] fn test_pattern_from_node() { - let compilation_context = - CompilationContext::new(None, GritTargetLanguage::JsTargetLanguage(JsTargetLanguage)); + let built_ins = BuiltIns::default(); + let compilation_context = CompilationContext::new( + None, + GritTargetLanguage::JsTargetLanguage(JsTargetLanguage), + &built_ins, + ); let mut vars = BTreeMap::new(); let mut vars_array = Vec::new(); let mut global_vars = BTreeMap::new(); @@ -798,8 +802,12 @@ mod tests { #[test] fn test_pattern_with_metavariables_from_node() { - let compilation_context = - CompilationContext::new(None, GritTargetLanguage::JsTargetLanguage(JsTargetLanguage)); + let built_ins = BuiltIns::default(); + let compilation_context = CompilationContext::new( + None, + GritTargetLanguage::JsTargetLanguage(JsTargetLanguage), + &built_ins, + ); let mut vars = BTreeMap::new(); let mut vars_array = vec![Vec::new()]; let mut global_vars = BTreeMap::new(); diff --git a/crates/biome_grit_patterns/tests/specs/ts/capitalize.grit b/crates/biome_grit_patterns/tests/specs/ts/capitalize.grit new file mode 100644 index 000000000000..f245e6aba86b --- /dev/null +++ b/crates/biome_grit_patterns/tests/specs/ts/capitalize.grit @@ -0,0 +1,3 @@ +`console.log($arg)` where { + $arg => capitalize(string = $arg) +} diff --git a/crates/biome_grit_patterns/tests/specs/ts/capitalize.snap b/crates/biome_grit_patterns/tests/specs/ts/capitalize.snap new file mode 100644 index 000000000000..68c701a4e46e --- /dev/null +++ b/crates/biome_grit_patterns/tests/specs/ts/capitalize.snap @@ -0,0 +1,12 @@ +--- +source: crates/biome_grit_patterns/tests/spec_tests.rs +expression: capitalize +--- +SnapshotResult { + messages: [], + matched_ranges: [ + "1:1-1:29", + ], + rewritten_files: [], + created_files: [], +} diff --git a/crates/biome_grit_patterns/tests/specs/ts/capitalize.ts b/crates/biome_grit_patterns/tests/specs/ts/capitalize.ts new file mode 100644 index 000000000000..8764baf693fc --- /dev/null +++ b/crates/biome_grit_patterns/tests/specs/ts/capitalize.ts @@ -0,0 +1 @@ +console.log('hello, world!');