|
| 1 | +use std::path::Path; |
| 2 | + |
| 3 | +use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string}; |
| 4 | +use ast_grep_core::replacer::Replacer; |
| 5 | +use ast_grep_language::{LanguageExt, SupportLang}; |
| 6 | +use serde_json::Value; |
| 7 | +use tokio::fs; |
| 8 | +use vite_error::Error; |
| 9 | + |
| 10 | +/// load script rules from yaml file |
| 11 | +async fn load_ast_grep_rules(yaml_path: &Path) -> Result<Vec<RuleConfig<SupportLang>>, Error> { |
| 12 | + let yaml = fs::read_to_string(yaml_path).await?; |
| 13 | + let globals = GlobalRules::default(); |
| 14 | + let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(&yaml, &globals)?; |
| 15 | + Ok(rules) |
| 16 | +} |
| 17 | + |
| 18 | +/// rewrite a single script command string using rules |
| 19 | +fn rewrite_script(script: &str, rules: &[RuleConfig<SupportLang>]) -> String { |
| 20 | + // current stores the current script text, and update it when the rule matches |
| 21 | + let mut current = script.to_string(); |
| 22 | + |
| 23 | + for rule in rules { |
| 24 | + // only handle bash rules |
| 25 | + if rule.language != SupportLang::Bash { |
| 26 | + continue; |
| 27 | + } |
| 28 | + |
| 29 | + // parse current script with corresponding language |
| 30 | + let grep = rule.language.ast_grep(¤t); |
| 31 | + let root = grep.root(); |
| 32 | + |
| 33 | + // this matcher is the AST matcher generated by deserializing the YAML rule |
| 34 | + let matcher = &rule.matcher; |
| 35 | + |
| 36 | + // rules may not have fix (pure lint), skip here |
| 37 | + let fixers = match rule.get_fixer() { |
| 38 | + Ok(f) if !f.is_empty() => f, |
| 39 | + _ => continue, |
| 40 | + }; |
| 41 | + |
| 42 | + // collect all matches and their replacements |
| 43 | + let mut replacements = Vec::new(); |
| 44 | + for node in root.find_all(matcher) { |
| 45 | + let range = node.range(); |
| 46 | + let replacement_bytes = fixers[0].generate_replacement(&node); |
| 47 | + let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string(); |
| 48 | + replacements.push((range.start, range.end, replacement_str)); |
| 49 | + } |
| 50 | + |
| 51 | + // Replace from back to front |
| 52 | + replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start)); |
| 53 | + |
| 54 | + for (start, end, replacement) in replacements { |
| 55 | + current.replace_range(start..end, &replacement); |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + current |
| 60 | +} |
| 61 | + |
| 62 | +/// rewrite scripts in package.json using rules from rules_yaml_path |
| 63 | +pub async fn rewrite_package_json_scripts( |
| 64 | + package_json_path: &Path, |
| 65 | + rules_yaml_path: &Path, |
| 66 | +) -> Result<bool, Error> { |
| 67 | + let content = fs::read_to_string(package_json_path).await?; |
| 68 | + let mut json: Value = serde_json::from_str(&content)?; |
| 69 | + let rules = load_ast_grep_rules(rules_yaml_path).await?; |
| 70 | + |
| 71 | + let mut updated = false; |
| 72 | + // get scripts field (object) |
| 73 | + if let Some(scripts) = json.get_mut("scripts").and_then(Value::as_object_mut) { |
| 74 | + let keys: Vec<String> = scripts.keys().cloned().collect(); |
| 75 | + for key in keys { |
| 76 | + if let Some(Value::String(script)) = scripts.get(&key) { |
| 77 | + let new_script = rewrite_script(script, &rules); |
| 78 | + if new_script != *script { |
| 79 | + updated = true; |
| 80 | + scripts.insert(key.clone(), Value::String(new_script)); |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + if updated { |
| 87 | + // write back to file |
| 88 | + let new_content = serde_json::to_string_pretty(&json)?; |
| 89 | + fs::write(package_json_path, new_content).await?; |
| 90 | + } |
| 91 | + |
| 92 | + Ok(updated) |
| 93 | +} |
| 94 | + |
| 95 | +#[cfg(test)] |
| 96 | +mod tests { |
| 97 | + use super::*; |
| 98 | + |
| 99 | + #[test] |
| 100 | + fn test_rewrite_script() { |
| 101 | + let yaml = r#" |
| 102 | +# vite => vite dev |
| 103 | +--- |
| 104 | +id: replace-vite-alone |
| 105 | +language: bash |
| 106 | +rule: |
| 107 | + kind: command |
| 108 | + has: |
| 109 | + kind: command_name |
| 110 | + regex: '^vite$' |
| 111 | + not: |
| 112 | + has: |
| 113 | + kind: word |
| 114 | + field: argument |
| 115 | +fix: vite dev |
| 116 | +
|
| 117 | +# vite [OPTIONS] => vite dev [OPTIONS] |
| 118 | +--- |
| 119 | +id: replace-vite-with-args |
| 120 | +language: bash |
| 121 | +severity: info |
| 122 | +rule: |
| 123 | + pattern: vite $$$ARGS |
| 124 | + not: |
| 125 | + # ignore non-flag arguments |
| 126 | + regex: 'vite\s+[^-]' |
| 127 | +fix: vite dev $$$ARGS |
| 128 | +
|
| 129 | +# oxlint => vite lint |
| 130 | +--- |
| 131 | +id: replace-oxlint-alone |
| 132 | +language: bash |
| 133 | +rule: |
| 134 | + kind: command |
| 135 | + has: |
| 136 | + kind: command_name |
| 137 | + regex: '^oxlint$' |
| 138 | + not: |
| 139 | + has: |
| 140 | + kind: word |
| 141 | + field: argument |
| 142 | +fix: vite lint |
| 143 | +
|
| 144 | +# oxlint [OPTIONS] => vite lint [OPTIONS] |
| 145 | +--- |
| 146 | +id: replace-oxlint-with-args |
| 147 | +language: bash |
| 148 | +rule: |
| 149 | + pattern: oxlint $$$ARGS |
| 150 | +fix: vite lint $$$ARGS |
| 151 | +"#; |
| 152 | + let globals = GlobalRules::default(); |
| 153 | + let rules: Vec<RuleConfig<SupportLang>> = |
| 154 | + from_yaml_string::<SupportLang>(&yaml, &globals).unwrap(); |
| 155 | + // vite commands |
| 156 | + assert_eq!(rewrite_script("vite", &rules), "vite dev"); |
| 157 | + assert_eq!(rewrite_script("vite dev", &rules), "vite dev"); |
| 158 | + assert_eq!(rewrite_script("vite i", &rules), "vite i"); |
| 159 | + assert_eq!(rewrite_script("vite install", &rules), "vite install"); |
| 160 | + assert_eq!(rewrite_script("vite test", &rules), "vite test"); |
| 161 | + assert_eq!(rewrite_script("vite lint", &rules), "vite lint"); |
| 162 | + assert_eq!(rewrite_script("vite fmt", &rules), "vite fmt"); |
| 163 | + assert_eq!(rewrite_script("vite lib", &rules), "vite lib"); |
| 164 | + assert_eq!(rewrite_script("vite preview", &rules), "vite preview"); |
| 165 | + assert_eq!(rewrite_script("vite optimize", &rules), "vite optimize"); |
| 166 | + assert_eq!(rewrite_script("vite build -r", &rules), "vite build -r"); |
| 167 | + assert_eq!(rewrite_script("vite --port 3000", &rules), "vite dev --port 3000"); |
| 168 | + assert_eq!( |
| 169 | + rewrite_script("vite --port 3000 --host 0.0.0.0 --open", &rules), |
| 170 | + "vite dev --port 3000 --host 0.0.0.0 --open" |
| 171 | + ); |
| 172 | + assert_eq!( |
| 173 | + rewrite_script("vite --port 3000 || vite --port 3001", &rules), |
| 174 | + "vite dev --port 3000 || vite dev --port 3001" |
| 175 | + ); |
| 176 | + assert_eq!( |
| 177 | + rewrite_script("npm run lint && vite --port 3000", &rules), |
| 178 | + "npm run lint && vite dev --port 3000" |
| 179 | + ); |
| 180 | + assert_eq!( |
| 181 | + rewrite_script("vite --port 3000 && npm run lint", &rules), |
| 182 | + "vite dev --port 3000 && npm run lint" |
| 183 | + ); |
| 184 | + assert_eq!( |
| 185 | + rewrite_script("vite && tsc --check && vite run -r build", &rules), |
| 186 | + "vite dev && tsc --check && vite run -r build" |
| 187 | + ); |
| 188 | + assert_eq!( |
| 189 | + rewrite_script("vite && tsc --check && vite run test", &rules), |
| 190 | + "vite dev && tsc --check && vite run test" |
| 191 | + ); |
| 192 | + assert_eq!( |
| 193 | + rewrite_script("vite && tsc --check && vite test", &rules), |
| 194 | + "vite dev && tsc --check && vite test" |
| 195 | + ); |
| 196 | + assert_eq!( |
| 197 | + rewrite_script("prettier --write src/** vite", &rules), |
| 198 | + "prettier --write src/** vite" |
| 199 | + ); |
| 200 | + // complex examples |
| 201 | + assert_eq!( |
| 202 | + rewrite_script("if [ -f file.txt ]; then vite; fi", &rules), |
| 203 | + "if [ -f file.txt ]; then vite dev; fi" |
| 204 | + ); |
| 205 | + assert_eq!( |
| 206 | + rewrite_script("if [ -f file.txt ]; then vite --port 3000; fi", &rules), |
| 207 | + "if [ -f file.txt ]; then vite dev --port 3000; fi" |
| 208 | + ); |
| 209 | + assert_eq!( |
| 210 | + rewrite_script("if [ -f file.txt ]; then vite --port 3000 && npm run lint; fi", &rules), |
| 211 | + "if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi" |
| 212 | + ); |
| 213 | + assert_eq!( |
| 214 | + rewrite_script( |
| 215 | + "if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi", |
| 216 | + &rules |
| 217 | + ), |
| 218 | + "if [ -f file.txt ]; then vite dev --port 3000 && npm run lint; fi" |
| 219 | + ); |
| 220 | + // oxlint commands |
| 221 | + assert_eq!(rewrite_script("oxlint", &rules), "vite lint"); |
| 222 | + assert_eq!(rewrite_script("oxlint --type-aware", &rules), "vite lint --type-aware"); |
| 223 | + assert_eq!( |
| 224 | + rewrite_script("oxlint --type-aware --config .oxlintrc", &rules), |
| 225 | + "vite lint --type-aware --config .oxlintrc" |
| 226 | + ); |
| 227 | + assert_eq!(rewrite_script("oxlint && vite dev", &rules), "vite lint && vite dev"); |
| 228 | + assert_eq!( |
| 229 | + rewrite_script("npm run type-check && oxlint --type-aware", &rules), |
| 230 | + "npm run type-check && vite lint --type-aware" |
| 231 | + ); |
| 232 | + } |
| 233 | +} |
0 commit comments