|
| 1 | +use crate::{ |
| 2 | + resolver::{resolve_from_cwd, GritModuleResolver, Source}, |
| 3 | + ux::{format_diff, DiffString}, |
| 4 | +}; |
| 5 | +use anyhow::{anyhow, bail, ensure, Context, Result}; |
| 6 | +use biome_grit_formatter::context::GritFormatOptions; |
| 7 | +use clap::Args; |
| 8 | +use colored::Colorize; |
| 9 | +use marzano_core::api::MatchResult; |
| 10 | +use marzano_gritmodule::{config::ResolvedGritDefinition, parser::PatternFileExt}; |
| 11 | +use marzano_util::{rich_path::RichFile, runtime::ExecutionContext}; |
| 12 | +use rayon::iter::{IntoParallelIterator, ParallelIterator}; |
| 13 | +use serde::Serialize; |
| 14 | +use std::collections::BTreeMap; |
| 15 | + |
| 16 | +#[derive(Args, Debug, Serialize, Clone)] |
| 17 | +pub struct FormatArgs { |
| 18 | + /// Write formats to file instead of just showing them |
| 19 | + #[clap(long)] |
| 20 | + pub write: bool, |
| 21 | +} |
| 22 | + |
| 23 | +pub async fn run_format(arg: &FormatArgs) -> Result<()> { |
| 24 | + let (resolved, _) = resolve_from_cwd(&Source::Local).await?; |
| 25 | + |
| 26 | + let file_path_to_resolved = group_resolved_patterns_by_group(resolved); |
| 27 | + let mut results = file_path_to_resolved |
| 28 | + .into_par_iter() |
| 29 | + .map(|(file_path, resolved_patterns)| { |
| 30 | + let result = format_file_resolved_patterns(&file_path, resolved_patterns, arg.clone()); |
| 31 | + (file_path, result) |
| 32 | + }) |
| 33 | + .collect::<Vec<_>>(); |
| 34 | + |
| 35 | + // sort outputs to ensure consistent stdout output |
| 36 | + // also avoid using sort_by_key to prevent additional cloning of file_path |
| 37 | + results.sort_by(|(file_path, _), (other_file_path, _)| file_path.cmp(other_file_path)); |
| 38 | + |
| 39 | + for (file_path, result) in results { |
| 40 | + match result { |
| 41 | + Err(error) => eprintln!("couldn't format '{}': {error:?}", file_path), |
| 42 | + Ok(Some(diff)) => println!("{}:\n{}", file_path.bold(), diff), |
| 43 | + Ok(None) => (), // `args.write` is true or file is already formated |
| 44 | + } |
| 45 | + } |
| 46 | + Ok(()) |
| 47 | +} |
| 48 | + |
| 49 | +fn group_resolved_patterns_by_group( |
| 50 | + resolved: Vec<ResolvedGritDefinition>, |
| 51 | +) -> Vec<(String, Vec<ResolvedGritDefinition>)> { |
| 52 | + resolved.into_iter().fold(Vec::new(), |mut acc, resolved| { |
| 53 | + let file_path = &resolved.config.path; |
| 54 | + if let Some((_, resolved_patterns)) = acc |
| 55 | + .iter_mut() |
| 56 | + .find(|(resolv_file_path, _)| resolv_file_path == file_path) |
| 57 | + { |
| 58 | + resolved_patterns.push(resolved); |
| 59 | + } else { |
| 60 | + acc.push((file_path.clone(), vec![resolved])); |
| 61 | + } |
| 62 | + acc |
| 63 | + }) |
| 64 | +} |
| 65 | + |
| 66 | +fn format_file_resolved_patterns( |
| 67 | + file_path: &str, |
| 68 | + patterns: Vec<ResolvedGritDefinition>, |
| 69 | + arg: FormatArgs, |
| 70 | +) -> Result<Option<DiffString>> { |
| 71 | + let first_pattern = patterns |
| 72 | + .first() |
| 73 | + .ok_or_else(|| anyhow!("patterns is empty"))?; |
| 74 | + let first_pattern_raw_data = first_pattern |
| 75 | + .config |
| 76 | + .raw |
| 77 | + .as_ref() |
| 78 | + .ok_or_else(|| anyhow!("pattern doesn't have raw data"))?; |
| 79 | + let old_file_content = &first_pattern_raw_data.content; |
| 80 | + |
| 81 | + let new_file_content = match first_pattern_raw_data.format { |
| 82 | + PatternFileExt::Yaml => format_yaml_file(&patterns, old_file_content)?, |
| 83 | + PatternFileExt::Grit => format_grit_code(old_file_content)?, |
| 84 | + PatternFileExt::Md => { |
| 85 | + let hunks = patterns |
| 86 | + .iter() |
| 87 | + .map(format_pattern_as_hunk_changes) |
| 88 | + .collect::<Result<Vec<HunkChange>>>()?; |
| 89 | + apply_hunk_changes(old_file_content, hunks) |
| 90 | + } |
| 91 | + }; |
| 92 | + |
| 93 | + if &new_file_content == old_file_content { |
| 94 | + return Ok(None); |
| 95 | + } |
| 96 | + |
| 97 | + if arg.write { |
| 98 | + std::fs::write(file_path, new_file_content).with_context(|| "could not write to file")?; |
| 99 | + Ok(None) |
| 100 | + } else { |
| 101 | + Ok(Some(format_diff(old_file_content, &new_file_content))) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +/// bubble clause that finds a grit pattern with name "\<pattern_name\>" in yaml and |
| 106 | +/// replaces it's body to "\<new_body\>", `format_yaml_file` uses this pattern to replace |
| 107 | +/// pattern bodies with formatted ones |
| 108 | +const YAML_REPLACE_BODY_PATERN: &str = r#" |
| 109 | + bubble file($body) where { |
| 110 | + $body <: contains block_mapping(items=$items) where { |
| 111 | + $items <: within `patterns: $_`, |
| 112 | + $items <: contains `name: $name`, |
| 113 | + $name <: "<pattern_name>", |
| 114 | + $items <: contains `body: $yaml_body`, |
| 115 | + $new_body = "<new_body>", |
| 116 | + $yaml_body => $new_body |
| 117 | + }, |
| 118 | + } |
| 119 | +"#; |
| 120 | + |
| 121 | +/// format each pattern and use gritql pattern to match and rewrite |
| 122 | +fn format_yaml_file(patterns: &[ResolvedGritDefinition], file_content: &str) -> Result<String> { |
| 123 | + let bubbles = patterns |
| 124 | + .iter() |
| 125 | + .map(|pattern| { |
| 126 | + let formatted_body = format_grit_code(&pattern.body) |
| 127 | + .with_context(|| format!("could not format '{}'", pattern.name()))?; |
| 128 | + let bubble = YAML_REPLACE_BODY_PATERN |
| 129 | + .replace("<pattern_name>", pattern.name()) |
| 130 | + .replace("<new_body>", &format_yaml_body_code(&formatted_body)); |
| 131 | + Ok(bubble) |
| 132 | + }) |
| 133 | + .collect::<Result<Vec<_>>>()? |
| 134 | + .join(",\n"); |
| 135 | + let pattern_body = format!("language yaml\nsequential{{ {bubbles} }}"); |
| 136 | + apply_grit_rewrite(file_content, &pattern_body) |
| 137 | +} |
| 138 | + |
| 139 | +fn format_yaml_body_code(input: &str) -> String { |
| 140 | + // yaml body still needs two indentation to look good |
| 141 | + let body_with_prefix = prefix_lines(input, &" ".repeat(2)); |
| 142 | + let escaped_body = body_with_prefix.replace("\"", "\\\""); |
| 143 | + // body: | |
| 144 | + // escaped_body |
| 145 | + format!("|\n{escaped_body}") |
| 146 | +} |
| 147 | + |
| 148 | +fn prefix_lines(input: &str, prefix: &str) -> String { |
| 149 | + input |
| 150 | + .lines() |
| 151 | + .map(|line| { |
| 152 | + if line.is_empty() { |
| 153 | + line.to_owned() |
| 154 | + } else { |
| 155 | + format!("{prefix}{line}") |
| 156 | + } |
| 157 | + }) |
| 158 | + .collect::<Vec<_>>() |
| 159 | + .join("\n") |
| 160 | +} |
| 161 | + |
| 162 | +fn apply_grit_rewrite(input: &str, pattern: &str) -> Result<String> { |
| 163 | + let resolver = GritModuleResolver::new(); |
| 164 | + let rich_pattern = resolver.make_pattern(pattern, None)?; |
| 165 | + |
| 166 | + let compiled = rich_pattern |
| 167 | + .compile(&BTreeMap::new(), None, None, None) |
| 168 | + .map(|cr| cr.problem) |
| 169 | + .with_context(|| "could not compile pattern")?; |
| 170 | + |
| 171 | + let rich_file = RichFile::new(String::new(), input.to_owned()); |
| 172 | + let runtime = ExecutionContext::default(); |
| 173 | + for result in compiled.execute_file(&rich_file, &runtime) { |
| 174 | + if let MatchResult::Rewrite(rewrite) = result { |
| 175 | + let content = rewrite |
| 176 | + .rewritten |
| 177 | + .content |
| 178 | + .ok_or_else(|| anyhow!("rewritten content is empty"))?; |
| 179 | + return Ok(content); |
| 180 | + } |
| 181 | + } |
| 182 | + bail!("no rewrite result after applying grit pattern") |
| 183 | +} |
| 184 | + |
| 185 | +fn format_pattern_as_hunk_changes(pattern: &ResolvedGritDefinition) -> Result<HunkChange> { |
| 186 | + let formatted_grit_code = format_grit_code(&pattern.body)?; |
| 187 | + let body_range = pattern |
| 188 | + .config |
| 189 | + .range |
| 190 | + .ok_or_else(|| anyhow!("pattern doesn't have config range"))?; |
| 191 | + Ok(HunkChange { |
| 192 | + starting_byte: body_range.start_byte as usize, |
| 193 | + ending_byte: body_range.end_byte as usize, |
| 194 | + new_content: formatted_grit_code, |
| 195 | + }) |
| 196 | +} |
| 197 | + |
| 198 | +/// format grit code using `biome` |
| 199 | +fn format_grit_code(source: &str) -> Result<String> { |
| 200 | + let parsed = biome_grit_parser::parse_grit(source); |
| 201 | + ensure!( |
| 202 | + parsed.diagnostics().is_empty(), |
| 203 | + "biome couldn't parse: {}", |
| 204 | + parsed |
| 205 | + .diagnostics() |
| 206 | + .iter() |
| 207 | + .map(|diag| diag.message.to_string()) |
| 208 | + .collect::<Vec<_>>() |
| 209 | + .join("\n") |
| 210 | + ); |
| 211 | + |
| 212 | + let options = GritFormatOptions::default(); |
| 213 | + let doc = biome_grit_formatter::format_node(options, &parsed.syntax()) |
| 214 | + .with_context(|| "biome couldn't format")?; |
| 215 | + Ok(doc.print()?.into_code()) |
| 216 | +} |
| 217 | + |
| 218 | +/// Represent a hunk of text that needs to be changed |
| 219 | +#[derive(Debug)] |
| 220 | +struct HunkChange { |
| 221 | + starting_byte: usize, |
| 222 | + ending_byte: usize, |
| 223 | + new_content: String, |
| 224 | +} |
| 225 | + |
| 226 | +/// returns a new string that applies hunk changes |
| 227 | +fn apply_hunk_changes(input: &str, mut hunks: Vec<HunkChange>) -> String { |
| 228 | + if hunks.is_empty() { |
| 229 | + return input.to_string(); |
| 230 | + } |
| 231 | + hunks.sort_by_key(|hunk| -(hunk.starting_byte as isize)); |
| 232 | + let mut buffer = input.to_owned(); |
| 233 | + for hunk in hunks { |
| 234 | + let hunk_range = hunk.starting_byte..hunk.ending_byte; |
| 235 | + buffer.replace_range(hunk_range, &hunk.new_content); |
| 236 | + } |
| 237 | + buffer |
| 238 | +} |
0 commit comments