From 9e7c24b6eb5ae0fd869af2da3033c7a27cf9bd80 Mon Sep 17 00:00:00 2001 From: Arian Rezazadeh Date: Fri, 10 Jan 2025 21:29:47 +0330 Subject: [PATCH] feat: use grit pattern to find and replace grit code in yaml --- Cargo.lock | 1 - crates/cli/Cargo.toml | 1 - crates/cli/src/commands/format.rs | 104 ++++++++++++++---- .../unformatted_patterns/.grit/grit.yaml | 9 ++ .../format__format_patterns_with_rewrite.snap | 2 +- 5 files changed, 92 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbf55302b..178f2f5cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2603,7 +2603,6 @@ dependencies = [ "reqwest 0.11.24", "serde", "serde_json", - "serde_yaml", "similar", "tempfile", "tokio", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 95ab2ce91..dde1773a0 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -22,7 +22,6 @@ indicatif = { version = "0.17.5" } # Do *NOT* upgrade beyond 1.0.171 until https://github.com/serde-rs/serde/issues/2538 is fixed serde = { version = "1.0.217", features = ["derive"] } serde_json = { version = "1.0.96" } -serde_yaml = { version = "0.9.25" } uuid = { version = "1.1", features = ["v4", "serde"] } tokio = { version = "1", features = ["full"] } chrono = { version = "0.4.26", features = ["serde"] } diff --git a/crates/cli/src/commands/format.rs b/crates/cli/src/commands/format.rs index f5bafdb02..91c5b8413 100644 --- a/crates/cli/src/commands/format.rs +++ b/crates/cli/src/commands/format.rs @@ -1,14 +1,17 @@ use crate::{ - resolver::{resolve_from_cwd, Source}, + resolver::{resolve_from_cwd, GritModuleResolver, Source}, ux::{format_diff, DiffString}, }; -use anyhow::{anyhow, ensure, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; use biome_grit_formatter::context::GritFormatOptions; use clap::Args; use colored::Colorize; +use marzano_core::api::MatchResult; use marzano_gritmodule::{config::ResolvedGritDefinition, parser::PatternFileExt}; +use marzano_util::{rich_path::RichFile, runtime::ExecutionContext}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use serde::Serialize; +use std::collections::BTreeMap; #[derive(Args, Debug, Serialize, Clone)] pub struct FormatArgs { @@ -78,7 +81,7 @@ fn format_file_resolved_patterns( let old_file_content = &first_pattern_raw_data.content; let new_file_content = match first_pattern_raw_data.format { - PatternFileExt::Yaml => format_yaml_file(old_file_content)?, + PatternFileExt::Yaml => format_yaml_file(patterns.clone(), old_file_content)?, PatternFileExt::Grit => format_grit_code(old_file_content)?, PatternFileExt::Md => { let hunks = patterns @@ -101,27 +104,84 @@ fn format_file_resolved_patterns( } } -fn format_yaml_file(file_content: &str) -> Result { - // deserializing manually and not using `SerializedGritConfig` because - // i don't want to remove any fields that `SerializedGritConfig` don't have such as 'version' - let mut config: serde_yaml::Value = - serde_yaml::from_str(file_content).with_context(|| "couldn't parse yaml file")?; - let patterns = config - .get_mut("patterns") - .ok_or_else(|| anyhow!("couldn't find patterns in yaml file"))? - .as_sequence_mut() - .ok_or_else(|| anyhow!("patterns in yaml file are not sequence"))?; - for pattern in patterns { - let Some(body) = pattern.get_mut("body") else { - continue; - }; - if let serde_yaml::Value::String(body_str) = body { - *body_str = format_grit_code(body_str)?; - // extra new line at end of grit body looks more readable - body_str.push('\n'); +/// bubble clause that finds a grit pattern with name "" in yaml and +/// replaces it's body to new_body, `format_yaml_file` uses this pattern to replace +/// pattern bodies with formatted ones +const YAML_REPLACE_BODY_PATERN: &'static str = r#" + bubble file($body) where { + $body <: contains block_mapping(items=$items) where { + $items <: within `patterns: $_`, + $items <: contains `name: $name`, + $name <: "", + $items <: contains `body: $yaml_body`, + $new_body = "", + $yaml_body => $new_body + }, + } +"#; + +/// format each pattern and use gritql pattern to match and rewrite +fn format_yaml_file(patterns: Vec, file_content: &str) -> Result { + let bubbles = patterns + .iter() + .map(|pattern| { + let formatted_body = format_grit_code(&pattern.body) + .with_context(|| format!("could not format '{}'", pattern.name()))?; + let bubble = YAML_REPLACE_BODY_PATERN + .replace("", pattern.name()) + .replace("", &format_yaml_body_code(&formatted_body)); + Ok(bubble) + }) + .collect::>>()? + .join(",\n"); + let pattern_body = format!("language yaml\nsequential{{ {bubbles} }}"); + apply_grit_rewrite(file_content, &pattern_body) +} + +fn format_yaml_body_code(input: &str) -> String { + // yaml body still needs two indentation to look good + let body_with_prefix = prefix_lines(&input, &" ".repeat(2)); + let escaped_body = body_with_prefix.replace("\"", "\\\""); + // body: | + // escaped_body + format!("|\n{escaped_body}") +} + +fn prefix_lines(input: &str, prefix: &str) -> String { + input + .lines() + .map(|line| { + if line.is_empty() { + line.to_owned() + } else { + format!("{prefix}{line}") + } + }) + .collect::>() + .join("\n") +} + +fn apply_grit_rewrite(input: &str, pattern: &str) -> Result { + let resolver = GritModuleResolver::new(); + let rich_pattern = resolver.make_pattern(&pattern, None)?; + + let compiled = rich_pattern + .compile(&BTreeMap::new(), None, None, None) + .map(|cr| cr.problem) + .with_context(|| "could not compile pattern")?; + + let rich_file = RichFile::new(String::new(), input.to_owned()); + let runtime = ExecutionContext::default(); + for result in compiled.execute_file(&rich_file, &runtime) { + if let MatchResult::Rewrite(rewrite) = result { + let content = rewrite + .rewritten + .content + .ok_or_else(|| anyhow!("rewritten content is empty"))?; + return Ok(content); } } - Ok(serde_yaml::to_string(&config)?) + bail!("no rewrite result after applying grit pattern") } fn format_pattern_as_hunk_changes(pattern: &ResolvedGritDefinition) -> Result { diff --git a/crates/cli_bin/fixtures/unformatted_patterns/.grit/grit.yaml b/crates/cli_bin/fixtures/unformatted_patterns/.grit/grit.yaml index e85ad7894..58db20033 100644 --- a/crates/cli_bin/fixtures/unformatted_patterns/.grit/grit.yaml +++ b/crates/cli_bin/fixtures/unformatted_patterns/.grit/grit.yaml @@ -10,3 +10,12 @@ patterns: } - file: ./others/test_move_import.md + + - name: some_json_pattern + body: | + language json + + `account: $val` where { + $val <: contains `password: $password`, + $password => raw`hidden` + } diff --git a/crates/cli_bin/tests/snapshots/format__format_patterns_with_rewrite.snap b/crates/cli_bin/tests/snapshots/format__format_patterns_with_rewrite.snap index 533fba4f7..e0f02c9d0 100644 --- a/crates/cli_bin/tests/snapshots/format__format_patterns_with_rewrite.snap +++ b/crates/cli_bin/tests/snapshots/format__format_patterns_with_rewrite.snap @@ -3,7 +3,7 @@ source: crates/cli_bin/tests/format.rs expression: "vec![yaml_file_content, test_move_import_file_content,\naspect_ratio_md_file_content, dependency_grit_file_content]" snapshot_kind: text --- -- "version: 0.0.1\npatterns:\n- name: aspect_ratio_yaml\n description: Yaml version of aspect_ratio.md\n body: |+\n language css;\n `a { $props }` where { $props <: contains `aspect-ratio: $x` }\n\n- file: ./others/test_move_import.md\n" +- "version: 0.0.1\npatterns:\n - name: aspect_ratio_yaml\n description: Yaml version of aspect_ratio.md\n body: |\n language css;\n `a { $props }` where { $props <: contains `aspect-ratio: $x` }\n\n - file: ./others/test_move_import.md\n\n - name: some_json_pattern\n body: |\n language json;\n `account: $val` where {\n \t$val <: contains `password: $password`,\n \t$password => raw`hidden`\n }" - "---\nprivate: true\ntags: [private]\n---\n```grit\nlanguage js;\n`sanitizeFilePath` as $s where {\n\tmove_import(`sanitizeFilePath`, `'@getgrit/universal'`)\n}\n```\n" - "---\ntitle: Aspect ratio\n---\n\n```grit\nlanguage css;\n`a { $props }` where { $props <: contains `aspect-ratio: $x` }\n```\n\n## Matches the right selector and declaration block\n\n```css\na {\n width: calc(100% - 80px);\n aspect-ratio: 1/2;\n font-size: calc(10px + (56 - 10) * ((100vw - 320px) / (1920 - 320)));\n}\n\n#some-id {\n some-property: 5px;\n}\n\na.b ~ c.d {\n}\n.e.f + .g.h {\n}\n\n@font-face {\n font-family: 'Open Sans';\n src: url('/a') format('woff2'), url('/b/c') format('woff');\n}\n```\n\n```css\na {\n width: calc(100% - 80px);\n aspect-ratio: 1/2;\n font-size: calc(10px + (56 - 10) * ((100vw - 320px) / (1920 - 320)));\n}\n\n#some-id {\n some-property: 5px;\n}\n\na.b ~ c.d {\n}\n.e.f + .g.h {\n}\n\n@font-face {\n font-family: 'Open Sans';\n src: url('/a') format('woff2'), url('/b/c') format('woff');\n}\n```\n" - "language json;\npattern upgrade_dependency($target_dep, $target_version, $dependency_key) {\n\tor {\n\t\t`$key: $value` where {\n\t\t\t$key <: `\"$target_dep\"`,\n\t\t\t$value => `\"$target_version\"`\n\t\t},\n\t\tpair($key, $value) where {\n\t\t\t$key <: `\"$dependency_key\"`,\n\t\t\t$value <: object($properties) where {\n\t\t\t\t$properties <: notcontains pair(key = $dep_key) where {\n\t\t\t\t\t$dep_key <: contains `$target_dep`\n\t\t\t\t},\n\t\t\t\t$properties => `\"$target_dep\": \"$target_version\",\\n$properties`\n\t\t\t}\n\t\t}\n\t}}\n\npattern console_method_to_info($method) {\n\t`console.$method($message)` => `console.info($message)`}\n"